From 6676fa3832f66f4097d02763215f942285279830 Mon Sep 17 00:00:00 2001 From: Alexey Lapin Date: Sun, 1 Oct 2023 18:12:31 +0200 Subject: [PATCH] 2.1.0. Force/Soft Deleting rows (#36) * feat: added a possibility to delete/force delete rows --- README.md | 132 ++++++- src/Builders/DeleteBulkBuilder.php | 41 ++ src/Builders/UpdateBulkBuilder.php | 7 + src/Bulk.php | 155 +++++++- src/Contracts/BulkDriver.php | 8 +- src/Drivers/MySql/MySqlDriverDelete.php | 32 ++ src/Drivers/MySqlBulkDriver.php | 10 +- .../PostgreSql/PostgreSqlDriverDelete.php | 30 ++ src/Drivers/PostgreSqlBulkDriver.php | 8 + src/Enums/BulkEventEnum.php | 41 +- src/Features/GetDeleteBuilderFeature.php | 78 ++++ src/Grammars/MySqlGrammar.php | 16 + src/Grammars/PostgreSqlGrammar.php | 10 + src/Scenarios/DeleteScenario.php | 183 +++++++++ ...eateBeforeWritingEventDependenciesTest.php | 20 +- tests/Unit/Bulk/Create/CreateEventsTest.php | 44 +-- .../Bulk/Create/CreateOrAccumulateTest.php | 4 +- tests/Unit/Bulk/Create/CreateTest.php | 4 +- tests/Unit/Bulk/Delete/DatabaseTest.php | 168 ++++++++ .../Bulk/Delete/DeleteAccumulatedTest.php | 138 +++++++ .../Bulk/Delete/DeleteOrAccumulateTest.php | 217 ++++++++++ tests/Unit/Bulk/Delete/FireEventsTest.php | 372 ++++++++++++++++++ .../Update/UpdateAfterWritingEventsTest.php | 10 +- ...dateBeforeWritingEventDependenciesTest.php | 30 +- tests/Unit/Bulk/Update/UpdateEventsTest.php | 44 +-- .../SelectAndUpdateManyTest.php | 6 +- tests/Unit/UserTestTrait.php | 53 ++- tests/app/Factories/PostFactory.php | 8 + tests/app/Models/Post.php | 2 + .../{UserObserver.php => Observer.php} | 38 +- 30 files changed, 1804 insertions(+), 105 deletions(-) create mode 100644 src/Builders/DeleteBulkBuilder.php create mode 100644 src/Drivers/MySql/MySqlDriverDelete.php create mode 100644 src/Drivers/PostgreSql/PostgreSqlDriverDelete.php create mode 100644 src/Features/GetDeleteBuilderFeature.php create mode 100644 src/Scenarios/DeleteScenario.php create mode 100644 tests/Unit/Bulk/Delete/DatabaseTest.php create mode 100644 tests/Unit/Bulk/Delete/DeleteAccumulatedTest.php create mode 100644 tests/Unit/Bulk/Delete/DeleteOrAccumulateTest.php create mode 100644 tests/Unit/Bulk/Delete/FireEventsTest.php rename tests/app/Observers/{UserObserver.php => Observer.php} (84%) diff --git a/README.md b/README.md index c40a43f..077491f 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Laravel doesn't return them. Of course, it won't be a big deal if you have only you will need some time to write quite large SQL query to select them in another case. Because of the above I have written this library which solves these problems. Using this library you can -save a collection of your models and use eloquent events such as `creating`, `created`, `updating`, -`updated`, `saving`, `saved`, `deleting`, `deleted`, `restoring`, `restored` at the same time. +save a collection of your models and use eloquent events such as `creating/created`, `updating/updated`, +`saving/saved`, `deleting/deleted`, `restoring/restored`, `forceDeleting/forceDeleted` at the same time. And you don't need to prepare the number of fields before. In simple terms, this library runs something like this: @@ -27,7 +27,7 @@ foreach($models as $model){ } ``` -but with 2-3 queries to the database per chunk. +but with a few queries to the database per chunk. ## Features - Creating / Updating / Upserting a collection with firing eloquent events: @@ -36,6 +36,7 @@ but with 2-3 queries to the database per chunk. - `saving` / `saved`, - `deleting` / `deleted`, - `restoring` / `restoried`; + - `forceDeleting` / `forceDeleted`; and some new events: - `creatingMany` / `createdMany`, @@ -43,6 +44,7 @@ but with 2-3 queries to the database per chunk. - `savingMany` / `savedMany`, - `deletingMany` / `deletedMany`, - `restoringMany` / `restoredMany`; + - `forceDeletingMany` / `forceDeletedMany`; - Automatically align transmitted fields before save them to the database event if you don't use eloquent events. - Select inserted rows from the database @@ -244,6 +246,47 @@ foreach($data as $item) { $bulk->upsertAccumulated(); ``` +### Force/Soft Deleting (since `v2.1.0`) + +Preparing the data + +```php +$data = [ + ['email' => 'jacob@example.com', 'name' => 'Jacob'], + ['id' => 1, 'email' => 'oscar@example.com', 'name' => 'Oscar'], +]; +$bulk->create($data); +``` + +You can just delete these users. +If your model uses the trait `Illuminate\Database\Eloquent\SoftDeletes`, then your model +will delete softly else force. +```php +$bulk->uniqueBy(['email']) + ->delete($data); +``` + +Or you can force delete them. +```php +$bulk->uniqueBy(['email']) + ->forceDelete($data); +``` + +You also can accumulate rows until there are enough of them to be deleted. +```php +$chunkSize = 100; +$bulk->uniqueBy(['email']) + ->chunk($chunkSize); + +foreach($data as $item) { + $bulk->deleteOrAccumulate($item); + // or $bulk->forceDeleteOrAccumulate($item); +} + +$bulk->deleteAccumulated(); +// or $bulk->forceDeleteAccumulated(); +``` + ### Listeners #### The order of events @@ -251,16 +294,20 @@ The order of calling callbacks is: - `onSaving` - `onCreating` or `onUpdating` - `onDeleting` +- `onForceDeleting` - `onRestoring` - `onSavingMany` - `onCreatingMany` or `onUpdatingMany` - `onDeletingMany` +- `onForceDeletingMany` - `onRestoringMany` - `onCreated` or `onUpdated` - `onDeleted` +- `onForceDeleted` - `onRestored` - `onCreatedMany` or `onUpdatedMany` - `onDeletedMany` +- `onForceDeletedMany` - `onRestoredMany` - `onSavedMany` @@ -296,9 +343,16 @@ $bulk // but it doesn't affect the upserting. ->onDeleting(fn(User $user) => /* ... */) + // The callback runs before force deleting. + // If your callback returns `false` then the model won't be deleted, + ->onForceDeleting(fn(User $user) => /* ... */) + // The callback runs after deleting. ->onDeleted(fn(User $user) => /* ... */) + // The callback runs after force deleting. + ->onForceDeleted(fn(User $user) => /* ... */) + // The callback runs before upserting. ->onSaving(fn(User $user) => /* ... */) @@ -324,9 +378,16 @@ $bulk // but it doesn't affect the upserting. ->onDeletingMany(fn(Collection $users, BulkRows $bulkRows) => /* .. */) + // Runs before force deleting. + // If the callback returns `false` then these models won't be deleted. + ->onForceDeletingMany(fn(Collection $users, BulkRows $bulkRows) => /* .. */) + // Runs after deleting. ->onDeletedMany(fn(Collection $users, BulkRows $bulkRows) => /* .. */) + // Runs after force deleting. + ->onForceDeletedMany(fn(Collection $users, BulkRows $bulkRows) => /* .. */) + // Runs before restoring. // If the callback returns `false` then these models won't be restored, // but it doesn't affect the upserting. @@ -558,6 +619,12 @@ class Bulk { */ public function createAndReturn(iterable $rows, array $columns = ['*'], bool $ignoreConflicts = false): Collection; + /** + * Creates the all accumulated rows. + * @throws BulkException + */ + public function createAccumulated(): static; + /** * Updates the rows. * @param iterable|object> $rows @@ -581,6 +648,12 @@ class Bulk { */ public function updateAndReturn(iterable $rows, array $columns = ['*']): Collection; + /** + * Updates the all accumulated rows. + * @throws BulkException + */ + public function updateAccumulated(): static; + /** * Upserts the rows. * @param iterable|object> $rows @@ -605,22 +678,58 @@ class Bulk { public function upsertAndReturn(iterable $rows, array $columns = ['*']): Collection; /** - * Creates the all accumulated rows. + * Upserts the all accumulated rows. * @throws BulkException */ - public function createAccumulated(): static; + public function upsertAccumulated(): static; /** - * Updates the all accumulated rows. + * Deletes the rows. + * @param iterable|object> $rows * @throws BulkException + * @since 2.1.0 */ - public function updateAccumulated(): static; + public function delete(iterable $rows): static; /** - * Upserts the all accumulated rows. + * Deletes the rows if their quantity is greater than or equal to the chunk size. + * @param iterable|object> $rows * @throws BulkException + * @since 2.1.0 */ - public function upsertAccumulated(): static; + public function deleteOrAccumulate(iterable $rows): static; + + /** + * Force deletes the rows. + * @param iterable|object> $rows + * @throws BulkException + * @since 2.1.0 + */ + public function forceDelete(iterable $rows): static; + + /** + * Force deletes the rows if their quantity is greater than or equal to the chunk size. + * @param iterable|object> $rows + * @throws BulkException + * @since 2.1.0 + */ + public function forceDeleteOrAccumulate(iterable $rows): static; + + /** + * Deletes the all accumulated rows. + * + * @throws BulkException + * @since 2.1.0 + */ + public function deleteAccumulated(): static; + + /** + * Deletes the all accumulated rows. + * + * @throws BulkException + * @since 2.1.0 + */ + public function forceDeleteAccumulated(): static; /** * Saves the all accumulated rows. @@ -655,11 +764,12 @@ class BulkRow { ``` ### TODO -* Bulk deleting * Bulk restoring * Bulk touching * Bulk updating without updating timestamps -* Supporting `DB::raw()` +* Supporting `DB::raw()` as a value +* Supporting `SQLite` +* Support a custom database driver ### Tests diff --git a/src/Builders/DeleteBulkBuilder.php b/src/Builders/DeleteBulkBuilder.php new file mode 100644 index 0000000..7b16f4b --- /dev/null +++ b/src/Builders/DeleteBulkBuilder.php @@ -0,0 +1,41 @@ +from = $table; + + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function limit(int $limit): static + { + $this->limit = $limit; + + return $this; + } + + public function getLimit(): ?int + { + return $this->limit; + } +} diff --git a/src/Builders/UpdateBulkBuilder.php b/src/Builders/UpdateBulkBuilder.php index 181d7cf..563cde1 100644 --- a/src/Builders/UpdateBulkBuilder.php +++ b/src/Builders/UpdateBulkBuilder.php @@ -70,6 +70,13 @@ public function addSet(string $field, array $filters, int|float|string|null|bool return $this; } + public function addSimpleSet(string $field, int|float|string|null|bool $value): static + { + $this->sets[$field] = $value; + + return $this; + } + public function getLimit(): ?int { return $this->limit; diff --git a/src/Bulk.php b/src/Bulk.php index efc0e80..39a20bd 100644 --- a/src/Bulk.php +++ b/src/Bulk.php @@ -21,6 +21,7 @@ use Lapaliv\BulkUpsert\Features\GetDateFieldsFeature; use Lapaliv\BulkUpsert\Features\GetDeletedAtColumnFeature; use Lapaliv\BulkUpsert\Scenarios\CreateScenario; +use Lapaliv\BulkUpsert\Scenarios\DeleteScenario; use Lapaliv\BulkUpsert\Scenarios\UpdateScenario; use Lapaliv\BulkUpsert\Scenarios\UpsertScenario; use stdClass; @@ -84,6 +85,8 @@ class Bulk 'create' => [], 'update' => [], 'upsert' => [], + 'delete' => [], + 'forceDelete' => [], ]; /** @@ -590,6 +593,88 @@ function (Collection $collection) use ($result): void { return $result; } + /** + * Deletes the rows. + * + * @param iterable|Model|object|stdClass|TModel> $rows + * + * @return $this + * + * @throws BulkBindingResolution + * @throws BulkException + */ + public function delete(iterable $rows): static + { + $this->accumulate('delete', $rows); + + foreach ($this->getReadyChunks('delete', force: true) as $accumulation) { + $this->runDeleteScenario($accumulation, force: false); + } + + return $this; + } + + /** + * Deletes the rows if their quantity is greater than or equal to the chunk size. + * + * @param iterable|Model|object|stdClass|TModel> $rows + * + * @return $this + * + * @throws BulkException + */ + public function deleteOrAccumulate(iterable $rows): static + { + $this->accumulate('delete', $rows); + + foreach ($this->getReadyChunks('delete') as $accumulation) { + $this->runDeleteScenario($accumulation, force: false); + } + + return $this; + } + + /** + * Force deletes the rows. + * + * @param iterable|Model|object|stdClass|TModel> $rows + * + * @return $this + * + * @throws BulkBindingResolution + * @throws BulkException + */ + public function forceDelete(iterable $rows): static + { + $this->accumulate('forceDelete', $rows); + + foreach ($this->getReadyChunks('forceDelete', force: true) as $accumulation) { + $this->runDeleteScenario($accumulation, force: true); + } + + return $this; + } + + /** + * Force deletes the rows if their quantity is greater than or equal to the chunk size. + * + * @param iterable|Model|object|stdClass|TModel> $rows + * + * @return $this + * + * @throws BulkException + */ + public function forceDeleteOrAccumulate(iterable $rows): static + { + $this->accumulate('forceDelete', $rows); + + foreach ($this->getReadyChunks('forceDelete') as $accumulation) { + $this->runDeleteScenario($accumulation, force: true); + } + + return $this; + } + /** * Creates the all accumulated rows. * @@ -642,6 +727,38 @@ public function upsertAccumulated(): static return $this; } + /** + * Deletes the all accumulated rows. + * + * @return $this + * + * @throws BulkException + */ + public function deleteAccumulated(): static + { + foreach ($this->getReadyChunks('delete', force: true) as $accumulation) { + $this->runDeleteScenario($accumulation, force: false); + } + + return $this; + } + + /** + * Deletes the all accumulated rows. + * + * @return $this + * + * @throws BulkException + */ + public function forceDeleteAccumulated(): static + { + foreach ($this->getReadyChunks('forceDelete', force: true) as $accumulation) { + $this->runDeleteScenario($accumulation, force: true); + } + + return $this; + } + /** * Saves the all accumulated rows. * @@ -653,7 +770,9 @@ public function saveAccumulated(): static { return $this->createAccumulated() ->updateAccumulated() - ->upsertAccumulated(); + ->upsertAccumulated() + ->deleteAccumulated() + ->forceDeleteAccumulated(); } /** @@ -876,6 +995,40 @@ private function runCreateScenario(BulkAccumulationEntity $accumulation, bool $i } } + /** + * Runs the delete scenario. + * + * @param BulkAccumulationEntity $accumulation + * @param bool $force + * + * @return void + * + * @throws BulkBindingResolution + */ + private function runDeleteScenario(BulkAccumulationEntity $accumulation, bool $force): void + { + try { + /** @var DeleteScenario $scenario */ + $scenario = Container::getInstance()->make(DeleteScenario::class); + $scenario->handle( + $this->model, + $accumulation, + $this->getEventDispatcher(), + $this->getDateFields(), + $this->getDeletedAtColumn(), + $force, + ); + + unset($scenario); + } catch (BindingResolutionException $exception) { + throw new BulkBindingResolution( + $exception->getMessage(), + $exception->getCode(), + $exception + ); + } + } + /** * Runs the update scenario. * diff --git a/src/Contracts/BulkDriver.php b/src/Contracts/BulkDriver.php index f42e7fa..e1d3030 100644 --- a/src/Contracts/BulkDriver.php +++ b/src/Contracts/BulkDriver.php @@ -3,6 +3,7 @@ namespace Lapaliv\BulkUpsert\Contracts; use Illuminate\Database\ConnectionInterface; +use Lapaliv\BulkUpsert\Builders\DeleteBulkBuilder; use Lapaliv\BulkUpsert\Builders\InsertBuilder; use Lapaliv\BulkUpsert\Builders\UpdateBulkBuilder; @@ -16,8 +17,7 @@ public function insertWithResult( public function quietInsert(ConnectionInterface $connection, InsertBuilder $builder): void; - public function update( - ConnectionInterface $connection, - UpdateBulkBuilder $builder - ): int; + public function update(ConnectionInterface $connection, UpdateBulkBuilder $builder): int; + + public function forceDelete(ConnectionInterface $connection, DeleteBulkBuilder $builder): int; } diff --git a/src/Drivers/MySql/MySqlDriverDelete.php b/src/Drivers/MySql/MySqlDriverDelete.php new file mode 100644 index 0000000..2117493 --- /dev/null +++ b/src/Drivers/MySql/MySqlDriverDelete.php @@ -0,0 +1,32 @@ +mixedValueToSqlConverter); + + $result = $connection->delete($grammar->delete($builder), $grammar->getBindings()); + + unset($grammar); + + return $result; + } +} diff --git a/src/Drivers/MySqlBulkDriver.php b/src/Drivers/MySqlBulkDriver.php index 0636802..8e33fc9 100644 --- a/src/Drivers/MySqlBulkDriver.php +++ b/src/Drivers/MySqlBulkDriver.php @@ -3,10 +3,12 @@ namespace Lapaliv\BulkUpsert\Drivers; use Illuminate\Database\ConnectionInterface; +use Lapaliv\BulkUpsert\Builders\DeleteBulkBuilder; use Lapaliv\BulkUpsert\Builders\InsertBuilder; use Lapaliv\BulkUpsert\Builders\UpdateBulkBuilder; use Lapaliv\BulkUpsert\Contracts\BulkDriver; use Lapaliv\BulkUpsert\Contracts\BulkInsertResult; +use Lapaliv\BulkUpsert\Drivers\MySql\MySqlDriverDelete; use Lapaliv\BulkUpsert\Drivers\MySql\MySqlDriverInsertWithResult; use Lapaliv\BulkUpsert\Drivers\MySql\MySqlDriverQuietInsert; use Lapaliv\BulkUpsert\Drivers\MySql\MySqlDriverUpdate; @@ -20,7 +22,8 @@ class MySqlBulkDriver implements BulkDriver public function __construct( private MySqlDriverInsertWithResult $insertWithResult, private MySqlDriverQuietInsert $quietInsert, - private MySqlDriverUpdate $update + private MySqlDriverUpdate $update, + private MySqlDriverDelete $delete, ) { // } @@ -51,4 +54,9 @@ public function update(ConnectionInterface $connection, UpdateBulkBuilder $build { return $this->update->handle($connection, $builder); } + + public function forceDelete(ConnectionInterface $connection, DeleteBulkBuilder $builder): int + { + return $this->delete->handle($connection, $builder); + } } diff --git a/src/Drivers/PostgreSql/PostgreSqlDriverDelete.php b/src/Drivers/PostgreSql/PostgreSqlDriverDelete.php new file mode 100644 index 0000000..16b0053 --- /dev/null +++ b/src/Drivers/PostgreSql/PostgreSqlDriverDelete.php @@ -0,0 +1,30 @@ +mixedValueToSqlConverter); + + $result = $connection->delete($grammar->delete($builder), $grammar->getBindings()); + + unset($grammar); + + return $result; + } +} diff --git a/src/Drivers/PostgreSqlBulkDriver.php b/src/Drivers/PostgreSqlBulkDriver.php index c5406d4..57d85ba 100644 --- a/src/Drivers/PostgreSqlBulkDriver.php +++ b/src/Drivers/PostgreSqlBulkDriver.php @@ -3,10 +3,12 @@ namespace Lapaliv\BulkUpsert\Drivers; use Illuminate\Database\ConnectionInterface; +use Lapaliv\BulkUpsert\Builders\DeleteBulkBuilder; use Lapaliv\BulkUpsert\Builders\InsertBuilder; use Lapaliv\BulkUpsert\Builders\UpdateBulkBuilder; use Lapaliv\BulkUpsert\Contracts\BulkDriver; use Lapaliv\BulkUpsert\Contracts\BulkInsertResult; +use Lapaliv\BulkUpsert\Drivers\PostgreSql\PostgreSqlDriverDelete; use Lapaliv\BulkUpsert\Drivers\PostgreSql\PostgreSqlDriverInsertWithResult; use Lapaliv\BulkUpsert\Drivers\PostgreSql\PostgreSqlDriverQuietInsert; use Lapaliv\BulkUpsert\Drivers\PostgreSql\PostgreSqlDriverUpdate; @@ -17,6 +19,7 @@ public function __construct( private PostgreSqlDriverInsertWithResult $insertWithResult, private PostgreSqlDriverQuietInsert $quietInsert, private PostgreSqlDriverUpdate $update, + private PostgreSqlDriverDelete $delete, ) { // } @@ -38,4 +41,9 @@ public function update(ConnectionInterface $connection, UpdateBulkBuilder $build { return $this->update->handle($connection, $builder); } + + public function forceDelete(ConnectionInterface $connection, DeleteBulkBuilder $builder): int + { + return $this->delete->handle($connection, $builder); + } } diff --git a/src/Enums/BulkEventEnum.php b/src/Enums/BulkEventEnum.php index 8ee9d4d..350d09f 100644 --- a/src/Enums/BulkEventEnum.php +++ b/src/Enums/BulkEventEnum.php @@ -23,6 +23,10 @@ class BulkEventEnum public const DELETED = 'deleted'; public const DELETING_MANY = 'deletingMany'; public const DELETED_MANY = 'deletedMany'; + public const FORCE_DELETING = 'forceDeleting'; + public const FORCE_DELETED = 'forceDeleted'; + public const FORCE_DELETING_MANY = 'forceDeletingMany'; + public const FORCE_DELETED_MANY = 'forceDeletedMany'; public const RESTORING = 'restoring'; public const RESTORED = 'restored'; public const RESTORING_MANY = 'restoringMany'; @@ -50,6 +54,10 @@ public static function cases(): array self::DELETED, self::DELETING_MANY, self::DELETED_MANY, + self::FORCE_DELETING, + self::FORCE_DELETED, + self::FORCE_DELETING_MANY, + self::FORCE_DELETED_MANY, self::RESTORING, self::RESTORED, self::RESTORING_MANY, @@ -71,6 +79,8 @@ public static function halt(): array self::SAVING_MANY, self::DELETING, self::DELETING_MANY, + self::FORCE_DELETING, + self::FORCE_DELETING_MANY, self::RESTORING, self::RESTORING_MANY, ]; @@ -90,6 +100,8 @@ public static function model(): array self::SAVED, self::DELETING, self::DELETED, + self::FORCE_DELETING, + self::FORCE_DELETED, self::RESTORING, self::RESTORED, ]; @@ -102,6 +114,7 @@ public static function modelEnd(): array self::UPDATED, self::SAVED, self::DELETED, + self::FORCE_DELETED, self::RESTORED, ]; } @@ -120,6 +133,8 @@ public static function collection(): array self::SAVED_MANY, self::DELETING_MANY, self::DELETED_MANY, + self::FORCE_DELETING_MANY, + self::FORCE_DELETED_MANY, self::RESTORING_MANY, self::RESTORED_MANY, ]; @@ -188,16 +203,25 @@ public static function updated(): array /** * @return string[] */ - public static function delete(): array + public static function delete(bool $force = false): array { - return array_merge(self::deleting(), self::deleted()); + return array_merge(self::deleting($force), self::deleted($force)); } /** * @return string[] */ - public static function deleting(): array + public static function deleting(bool $force = false): array { + if ($force) { + return [ + self::DELETING, + self::DELETING_MANY, + self::FORCE_DELETING, + self::FORCE_DELETING_MANY, + ]; + } + return [ self::DELETING, self::DELETING_MANY, @@ -207,8 +231,17 @@ public static function deleting(): array /** * @return string[] */ - public static function deleted(): array + public static function deleted(bool $force = false): array { + if ($force) { + return [ + self::DELETED, + self::DELETED_MANY, + self::FORCE_DELETED, + self::FORCE_DELETED_MANY, + ]; + } + return [ self::DELETED, self::DELETED_MANY, diff --git a/src/Features/GetDeleteBuilderFeature.php b/src/Features/GetDeleteBuilderFeature.php new file mode 100644 index 0000000..5141016 --- /dev/null +++ b/src/Features/GetDeleteBuilderFeature.php @@ -0,0 +1,78 @@ +getDeleteBuilder($eloquent, $data); + } + + return $this->getUpdateBuilder($eloquent, $data, $dateFields, $deletedAtColumn, $deletedAt); + } + + private function getDeleteBuilder( + Model $eloquent, + BulkAccumulationEntity $data + ): ?DeleteBulkBuilder { + $models = $data->getNotSkippedModels('skipDeleting'); + + if ($models->isEmpty()) { + return null; + } + + $result = new DeleteBulkBuilder(); + $result->from($eloquent->getTable()); + $this->addWhereClauseToBuilderFeature->handle($result, $data->uniqueBy, $models); + + return $result->limit($models->count()); + } + + private function getUpdateBuilder( + Model $eloquent, + BulkAccumulationEntity $data, + array $dateFields, + string $deletedAtColumn, + DateTimeInterface $deletedAt, + ): ?UpdateBulkBuilder { + $models = $data->getNotSkippedModels('skipDeleting'); + $result = new UpdateBulkBuilder(); + $result->table($eloquent->getTable()); + + if ($models->isEmpty()) { + return null; + } + + $result->addSimpleSet( + $deletedAtColumn, + $deletedAt->format($dateFields[$deletedAtColumn] ?? 'Y-m-d H:i:s') + ); + + $this->addWhereClauseToBuilderFeature->handle($result, $data->uniqueBy, $models); + + return $result->limit($models->count()); + } +} diff --git a/src/Grammars/MySqlGrammar.php b/src/Grammars/MySqlGrammar.php index 74b8074..d2ff26e 100644 --- a/src/Grammars/MySqlGrammar.php +++ b/src/Grammars/MySqlGrammar.php @@ -6,6 +6,7 @@ use Lapaliv\BulkUpsert\Builders\Clauses\Where\BuilderWhereCallback; use Lapaliv\BulkUpsert\Builders\Clauses\Where\BuilderWhereCondition; use Lapaliv\BulkUpsert\Builders\Clauses\Where\BuilderWhereIn; +use Lapaliv\BulkUpsert\Builders\DeleteBulkBuilder; use Lapaliv\BulkUpsert\Builders\InsertBuilder; use Lapaliv\BulkUpsert\Builders\SelectBulkBuilder; use Lapaliv\BulkUpsert\Builders\UpdateBulkBuilder; @@ -97,6 +98,21 @@ public function update(UpdateBulkBuilder $builder): string return $sql; } + public function delete(DeleteBulkBuilder $builder): string + { + $sql = sprintf( + 'delete from %s where %s', + $builder->getFrom(), + $this->getSqlWhereClause($builder->getWheres()) + ); + + if ($builder->getLimit() !== null) { + $sql .= sprintf(' limit %d', $builder->getLimit()); + } + + return $sql; + } + public function getBindings(): array { return $this->bindings; diff --git a/src/Grammars/PostgreSqlGrammar.php b/src/Grammars/PostgreSqlGrammar.php index 180cc05..6db64df 100644 --- a/src/Grammars/PostgreSqlGrammar.php +++ b/src/Grammars/PostgreSqlGrammar.php @@ -6,6 +6,7 @@ use Lapaliv\BulkUpsert\Builders\Clauses\Where\BuilderWhereCallback; use Lapaliv\BulkUpsert\Builders\Clauses\Where\BuilderWhereCondition; use Lapaliv\BulkUpsert\Builders\Clauses\Where\BuilderWhereIn; +use Lapaliv\BulkUpsert\Builders\DeleteBulkBuilder; use Lapaliv\BulkUpsert\Builders\InsertBuilder; use Lapaliv\BulkUpsert\Builders\SelectBulkBuilder; use Lapaliv\BulkUpsert\Builders\UpdateBulkBuilder; @@ -92,6 +93,15 @@ public function update(UpdateBulkBuilder $builder): string ); } + public function delete(DeleteBulkBuilder $builder): string + { + return sprintf( + 'delete from %s where %s', + $builder->getFrom(), + $this->getSqlWhereClause($builder->getWheres()) + ); + } + public function getBindings(): array { return $this->bindings; diff --git a/src/Scenarios/DeleteScenario.php b/src/Scenarios/DeleteScenario.php new file mode 100644 index 0000000..206cb34 --- /dev/null +++ b/src/Scenarios/DeleteScenario.php @@ -0,0 +1,183 @@ +rows)) { + return; + } + + $this->markNonexistentRowsAsSkipped->handle($eloquent, $data, $data->uniqueBy, $deletedAtColumn, true); + + if ($eventDispatcher->hasListeners(BulkEventEnum::delete($force))) { + $this->dispatchDeletingEvents($eloquent, $data, $eventDispatcher, $deletedAtColumn, $force); + } + + $deletedAt = Carbon::now(); + $builder = $this->getDeleteBuilderFeature->handle( + $eloquent, + $data, + $dateFields, + $deletedAtColumn, + $force, + $deletedAt, + ); + $driver = $this->driverManager->getForModel($eloquent); + + if ($builder instanceof UpdateBulkBuilder) { + $driver->update($eloquent->getConnection(), $builder); + } elseif ($builder instanceof DeleteBulkBuilder) { + $driver->forceDelete($eloquent->getConnection(), $builder); + } else { + unset($builder, $driver); + + return; + } + + unset($builder); + + $hasEndListeners = $eventDispatcher->hasListeners(BulkEventEnum::deleted($force)); + + if ($hasEndListeners) { + $this->dispatchDeletedEvents( + $eloquent, + $data, + $eventDispatcher, + $deletedAtColumn, + $force, + $deletedAt, + ); + } + } + + private function dispatchDeletingEvents( + Model $eloquent, + BulkAccumulationEntity $data, + BulkEventDispatcher $eventDispatcher, + ?string $deletedAtColumn, + bool $force + ): void { + $collection = $eloquent->newCollection(); + $bulkRows = new BulkRows(); + + foreach ($data->rows as $row) { + if ($row->skipDeleting) { + continue; + } + + if ($deletedAtColumn !== null + && $force + && $eventDispatcher->dispatch(BulkEventEnum::FORCE_DELETING, $row->model) === false + ) { + $row->skipDeleting = true; + + continue; + } + + if ($eventDispatcher->dispatch(BulkEventEnum::DELETING, $row->model) === false) { + $row->skipDeleting = true; + + continue; + } + + $collection->push($row->model); + $bulkRows->push( + new BulkRow($row->model, $row->row, $data->uniqueBy) + ); + } + + if ($deletedAtColumn !== null + && $force + && $eventDispatcher->dispatch(BulkEventEnum::FORCE_DELETING_MANY, $collection, $bulkRows) === false + ) { + foreach ($data->rows as $row) { + $row->skipDeleting = true; + } + + return; + } + + if ($eventDispatcher->dispatch(BulkEventEnum::DELETING_MANY, $collection, $bulkRows) === false) { + foreach ($data->rows as $row) { + $row->skipDeleting = true; + } + } + + unset($collection, $bulkRows); + } + + private function dispatchDeletedEvents( + Model $eloquent, + BulkAccumulationEntity $data, + BulkEventDispatcher $eventDispatcher, + ?string $deletedAtColumn, + bool $force, + DateTime $deletedAt + ): void { + $collection = $eloquent->newCollection(); + $bulkRows = new BulkRows(); + + foreach ($data->rows as $row) { + if ($row->skipDeleting) { + continue; + } + + if ($deletedAtColumn) { + $row->model->setAttribute($deletedAtColumn, $deletedAt); + } + + $eventDispatcher->dispatch(BulkEventEnum::DELETED, $row->model); + + if ($deletedAtColumn !== null && $force) { + $eventDispatcher->dispatch(BulkEventEnum::FORCE_DELETED, $row->model); + } + + $collection->push($row->model); + $bulkRows->push( + new BulkRow($row->model, $row->row, $data->uniqueBy) + ); + } + + $eventDispatcher->dispatch(BulkEventEnum::DELETED_MANY, $collection, $bulkRows); + + if ($deletedAtColumn !== null && $force) { + $eventDispatcher->dispatch(BulkEventEnum::FORCE_DELETED_MANY, $collection, $bulkRows); + } + + unset($collection, $bulkRows); + } +} diff --git a/tests/Unit/Bulk/Create/CreateBeforeWritingEventDependenciesTest.php b/tests/Unit/Bulk/Create/CreateBeforeWritingEventDependenciesTest.php index b4020c2..db213cf 100644 --- a/tests/Unit/Bulk/Create/CreateBeforeWritingEventDependenciesTest.php +++ b/tests/Unit/Bulk/Create/CreateBeforeWritingEventDependenciesTest.php @@ -13,7 +13,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\App\Support\TestCallback; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -46,7 +46,7 @@ public function testModelEventReturnsFalseSometimes(string $model, Closure $data { // arrange $users = $data(); - $model::observe(UserObserver::class); + $model::observe(Observer::class); /** @var array $spies */ $spies = [ $event => Mockery::mock(TestCallback::class), @@ -55,12 +55,12 @@ public function testModelEventReturnsFalseSometimes(string $model, Closure $data ->expects('__invoke') ->times(count($users)) ->andReturnValues([false, true]); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); foreach ($dependencies as $dependencyList) { foreach ($dependencyList as $dependency) { $spies[$dependency] = Mockery::spy(TestCallback::class, $dependency); - UserObserver::listen($dependency, $spies[$dependency]); + Observer::listen($dependency, $spies[$dependency]); } } @@ -115,7 +115,7 @@ public function testModelEventReturnsFalseAlways(string $model, Closure $data, s { // arrange $users = $data(); - $model::observe(UserObserver::class); + $model::observe(Observer::class); /** @var array $spies */ $spies = [ $event => Mockery::mock(TestCallback::class), @@ -124,12 +124,12 @@ public function testModelEventReturnsFalseAlways(string $model, Closure $data, s ->expects('__invoke') ->times(count($users)) ->andReturnFalse(); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); foreach ($dependencies as $dependencyList) { foreach ($dependencyList as $dependency) { $spies[$dependency] = Mockery::spy(TestCallback::class, $dependency); - UserObserver::listen($dependency, $spies[$dependency]); + Observer::listen($dependency, $spies[$dependency]); } } @@ -168,7 +168,7 @@ public function testCollectionEventReturnsFalse(string $model, Closure $data, st { // arrange $users = $data(); - $model::observe(UserObserver::class); + $model::observe(Observer::class); /** @var array $spies */ $spies = [ $event => Mockery::mock(TestCallback::class), @@ -177,12 +177,12 @@ public function testCollectionEventReturnsFalse(string $model, Closure $data, st ->expects('__invoke') ->once() ->andReturnFalse(); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); foreach ($dependencies as $dependencyList) { foreach ($dependencyList as $dependency) { $spies[$dependency] = Mockery::spy(TestCallback::class, $dependency); - UserObserver::listen($dependency, $spies[$dependency]); + Observer::listen($dependency, $spies[$dependency]); } } diff --git a/tests/Unit/Bulk/Create/CreateEventsTest.php b/tests/Unit/Bulk/Create/CreateEventsTest.php index 6ebb28f..1e516e9 100644 --- a/tests/Unit/Bulk/Create/CreateEventsTest.php +++ b/tests/Unit/Bulk/Create/CreateEventsTest.php @@ -7,7 +7,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\App\Support\TestCallback; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -42,8 +42,8 @@ public function testDisableAllEvents(string $model): void $spy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($spy); + $model::observe(Observer::class); + Observer::listenAny($spy); // act $sut->create($users); @@ -76,9 +76,9 @@ public function testDisableSomeEvents(string $model, string $disabledEvent): voi $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); - UserObserver::listen($disabledEvent, $notCallingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); + Observer::listen($disabledEvent, $notCallingSpy); // act $sut->create($users); @@ -114,11 +114,11 @@ public function testDisableModelEndEvents(string $model): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); foreach ($modelEndEvents as $event) { - UserObserver::listen($event, $notCallingSpy); + Observer::listen($event, $notCallingSpy); } // act @@ -155,9 +155,9 @@ public function testDisableOneEvent(string $model, string $disabledEvent): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); - UserObserver::listen($disabledEvent, $notCallingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); + Observer::listen($disabledEvent, $notCallingSpy); // act $sut->create($users); @@ -191,8 +191,8 @@ public function testEnableAllEvents(string $model): void $callingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); $countEventsPerModel = count(BulkEventEnum::create()) + count(BulkEventEnum::save()); @@ -232,8 +232,8 @@ public function testEnableSomeDisabledEvents(string $model, string $enabledEvent $callingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listen($enabledEvent, $callingSpy); + $model::observe(Observer::class); + Observer::listen($enabledEvent, $callingSpy); // act $sut->create($users); @@ -267,9 +267,9 @@ public function testEnableSomeEvents(string $model, string $enabledEvent): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($notCallingSpy, [$enabledEvent]); - UserObserver::listen($enabledEvent, $callingSpy); + $model::observe(Observer::class); + Observer::listenAny($notCallingSpy, [$enabledEvent]); + Observer::listen($enabledEvent, $callingSpy); // act $sut->create($users); @@ -304,9 +304,9 @@ public function testEnableOneEvent(string $model, string $enabledEvent): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($notCallingSpy, [$enabledEvent]); - UserObserver::listen($enabledEvent, $callingSpy); + $model::observe(Observer::class); + Observer::listenAny($notCallingSpy, [$enabledEvent]); + Observer::listen($enabledEvent, $callingSpy); // act $sut->create($users); diff --git a/tests/Unit/Bulk/Create/CreateOrAccumulateTest.php b/tests/Unit/Bulk/Create/CreateOrAccumulateTest.php index 25a7888..0490f8f 100644 --- a/tests/Unit/Bulk/Create/CreateOrAccumulateTest.php +++ b/tests/Unit/Bulk/Create/CreateOrAccumulateTest.php @@ -8,7 +8,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -150,7 +150,7 @@ public function testCreatingWithoutUniqueAttributesWithEvents(string $model): vo { // arrange $users = $this->userGenerator->makeCollection(2); - $model::observe(UserObserver::class); + $model::observe(Observer::class); $sut = $model::query()->bulk(); // assert diff --git a/tests/Unit/Bulk/Create/CreateTest.php b/tests/Unit/Bulk/Create/CreateTest.php index 16a5cf6..6ffcffb 100644 --- a/tests/Unit/Bulk/Create/CreateTest.php +++ b/tests/Unit/Bulk/Create/CreateTest.php @@ -10,7 +10,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -185,7 +185,7 @@ public function testCreatingWithoutUniqueAttributesWithEvents(string $model): vo { // arrange $users = $this->userGenerator->makeCollection(2); - $model::observe(UserObserver::class); + $model::observe(Observer::class); $sut = $model::query()->bulk(); // assert diff --git a/tests/Unit/Bulk/Delete/DatabaseTest.php b/tests/Unit/Bulk/Delete/DatabaseTest.php new file mode 100644 index 0000000..58a3c54 --- /dev/null +++ b/tests/Unit/Bulk/Delete/DatabaseTest.php @@ -0,0 +1,168 @@ + $model + * + * @return void + * + * @throws JsonException + * @throws BulkException + * + * @dataProvider userModelsDataProvider + */ + public function testDeleteWithSoftDeleting(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query() + ->bulk() + ->uniqueBy(['email']); + + // act + $sut->delete($users); + + // assert + foreach ($users as $user) { + $this->assertDatabaseHas($user->getTable(), [ + 'email' => $user->email, + 'name' => $user->name, + 'gender' => $user->gender->value, + 'avatar' => $user->avatar, + 'posts_count' => $user->posts_count, + 'is_admin' => $user->is_admin, + 'balance' => $user->balance, + 'birthday' => $user->birthday, + 'phones' => $user->phones, + 'last_visited_at' => $user->last_visited_at, + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), + 'deleted_at' => Carbon::now()->toDateTimeString(), + ], $user->getConnectionName()); + } + } + + /** + * @param class-string $model + * + * @return void + * + * @throws JsonException + * @throws BulkException + * + * @dataProvider userModelsDataProvider + */ + public function testForceDeleteWithSoftDeleting(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query() + ->bulk() + ->uniqueBy(['email']); + + // act + $sut->forceDelete($users); + + // assert + foreach ($users as $user) { + $this->assertDatabaseMissing($user->getTable(), [ + 'email' => $user->email, + 'name' => $user->name, + 'gender' => $user->gender->value, + 'avatar' => $user->avatar, + 'posts_count' => $user->posts_count, + 'is_admin' => $user->is_admin, + 'balance' => $user->balance, + 'birthday' => $user->birthday, + 'phones' => $user->phones, + 'last_visited_at' => $user->last_visited_at, + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), + 'deleted_at' => Carbon::now()->toDateTimeString(), + ], $user->getConnectionName()); + } + } + + /** + * @param class-string $model + * @param string $method + * + * @return void + * + * @throws BulkException + * @throws JsonException + * @throws BulkBindingResolution + * + * @dataProvider postModelsDataProvider + */ + public function testDeleteWithoutSoftDeleting(string $model, string $method): void + { + // arrange + $posts = $model::factory()->count(2)->create(); + $sut = $model::query()->bulk(); + + // act + $sut->{$method}($posts); + + // assert + foreach ($posts as $post) { + $this->assertDatabaseMissing($post->getTable(), [ + 'id' => $post->id, + ], $post->getConnectionName()); + } + } + + public function userModelsDataProvider(): array + { + return [ + 'mysql' => [MySqlUser::class], + 'postgresql' => [MySqlUser::class], + ]; + } + + public function postModelsDataProvider(): array + { + return [ + 'mysql, delete' => [ + MySqlPost::class, + 'delete', + ], + 'mysql, forceDelete' => [ + MySqlPost::class, + 'forceDelete', + ], + 'postgresql, delete' => [ + PostgreSqlPost::class, + 'delete', + ], + 'postgresql, forceDelete' => [ + PostgreSqlPost::class, + 'forceDelete', + ], + ]; + } +} diff --git a/tests/Unit/Bulk/Delete/DeleteAccumulatedTest.php b/tests/Unit/Bulk/Delete/DeleteAccumulatedTest.php new file mode 100644 index 0000000..608fe1a --- /dev/null +++ b/tests/Unit/Bulk/Delete/DeleteAccumulatedTest.php @@ -0,0 +1,138 @@ + $model + * + * @return void + * + * @dataProvider userModelsDataProvider + * + * @throws BulkException + */ + public function testDeleteAccumulatedWithSoftDeleting(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query() + ->bulk() + ->deleteOrAccumulate($users); + + // act + $sut->deleteAccumulated(); + + // assert + $users->each( + fn (User $user) => $this->userWasSoftDeleted($user) + ); + } + + /** + * @param class-string $model + * + * @return void + * + * @dataProvider userModelsDataProvider + * + * @throws BulkException + */ + public function testForceDeleteAccumulatedWithSoftDeleting(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query() + ->bulk() + ->forceDeleteOrAccumulate($users); + + // act + $sut->forceDeleteAccumulated(); + + // assert + $users->each( + fn (User $user) => $this->userDoesNotExist($user) + ); + } + + /** + * @param class-string $model + * @param string $accumulateMethod + * @param string $deleteMethod + * + * @return void + * + * @throws BulkException + * @throws JsonException + * + * @dataProvider postModelsDataProvider + */ + public function testDeleteAccumulatedWithoutSoftDeleting( + string $model, + string $accumulateMethod, + string $deleteMethod, + ): void { + // arrange + $posts = $model::factory() + ->count(2) + ->create(); + $sut = $model::query() + ->bulk() + ->{$accumulateMethod}($posts); + + // act + $sut->{$deleteMethod}(); + + // assert + $posts->each( + fn (Post $post) => $this->assertDatabaseMissing($post->getTable(), [ + 'id' => $post->id, + ], $post->getConnectionName()) + ); + } + + public function userModelsDataProvider(): array + { + return [ + 'mysql' => [MySqlUser::class], + 'postgresql' => [PostgreSqlUser::class], + ]; + } + + public function postModelsDataProvider(): array + { + return [ + 'mysql' => [ + MySqlPost::class, + 'deleteOrAccumulate', + 'deleteAccumulated', + ], + 'postgresql' => [ + PostgreSqlPost::class, + 'forceDeleteOrAccumulate', + 'forceDeleteAccumulated', + ], + ]; + } +} diff --git a/tests/Unit/Bulk/Delete/DeleteOrAccumulateTest.php b/tests/Unit/Bulk/Delete/DeleteOrAccumulateTest.php new file mode 100644 index 0000000..1734370 --- /dev/null +++ b/tests/Unit/Bulk/Delete/DeleteOrAccumulateTest.php @@ -0,0 +1,217 @@ + $model + * + * @return void + * + * @throws BulkException + * + * @dataProvider userModelsDataProvider + */ + public function testDeleteWithSoftDeletingSmallChunk(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query()->bulk(); + + // act + $sut->deleteOrAccumulate($users); + + // assert + $users->each( + fn (User $user) => $this->userExists($user) + ); + } + + /** + * @param class-string $model + * + * @return void + * + * @throws BulkException + * + * @dataProvider userModelsDataProvider + */ + public function testForceDeleteWithSoftDeletingSmallChunk(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query()->bulk(); + + // act + $sut->forceDeleteOrAccumulate($users); + + // assert + $users->each( + fn (User $user) => $this->userExists($user) + ); + } + + /** + * @param class-string $model + * + * @return void + * + * @throws BulkException + * + * @dataProvider userModelsDataProvider + */ + public function testDeleteWithSoftDeletingBigChunk(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query() + ->bulk() + ->chunk($users->count()); + + // act + $sut->deleteOrAccumulate($users); + + // assert + $users->each( + fn (User $user) => $this->userWasSoftDeleted($user) + ); + } + + /** + * @param class-string $model + * + * @return void + * + * @throws BulkException + * + * @dataProvider userModelsDataProvider + */ + public function testForceDeleteWithSoftDeletingBigChunk(string $model): void + { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query() + ->bulk() + ->chunk($users->count()); + + // act + $sut->forceDeleteOrAccumulate($users); + + // assert + $users->each( + fn (User $user) => $this->userDoesNotExist($user) + ); + } + + /** + * @param class-string $model + * @param string $method + * + * @return void + * + * @throws BulkException + * @throws JsonException + * + * @dataProvider postModelsDataProvider + */ + public function testDeleteWithoutSoftDeletingSmallChunk(string $model, string $method): void + { + // arrange + $posts = $model::factory()->count(2)->create(); + $sut = $model::query()->bulk(); + + // act + $sut->{$method}($posts); + + // assert + $posts->each( + fn (Post $post) => $this->assertDatabaseHas($post->getTable(), [ + 'id' => $post->id, + ], $post->getConnectionName()) + ); + } + + /** + * @param class-string $model + * @param string $method + * + * @return void + * + * @throws BulkException + * + * @dataProvider postModelsDataProvider + */ + public function testDeleteWithoutSoftDeletingBigChunk(string $model, string $method): void + { + // arrange + $posts = $model::factory()->count(2)->create(); + $sut = $model::query() + ->bulk() + ->chunk($posts->count()); + + // act + $sut->{$method}($posts); + + // assert + $posts->each( + fn (Post $post) => $this->assertDatabaseMissing($post->getTable(), [ + 'id' => $post->id, + ], $post->getConnectionName()) + ); + } + + public function userModelsDataProvider(): array + { + return [ + 'mysql' => [MySqlUser::class], + 'postgresql' => [PostgreSqlUser::class], + ]; + } + + public function postModelsDataProvider(): array + { + return [ + 'mysql, deleteOrAccumulate' => [ + MySqlPost::class, + 'deleteOrAccumulate', + ], + 'mysql, forceDeleteOrAccumulate' => [ + MySqlPost::class, + 'forceDeleteOrAccumulate', + ], + 'postgresql, deleteOrAccumulate' => [ + PostgreSqlPost::class, + 'deleteOrAccumulate', + ], + 'postgresql, forceDeleteOrAccumulate' => [ + PostgreSqlPost::class, + 'forceDeleteOrAccumulate', + ], + ]; + } +} diff --git a/tests/Unit/Bulk/Delete/FireEventsTest.php b/tests/Unit/Bulk/Delete/FireEventsTest.php new file mode 100644 index 0000000..0be3f21 --- /dev/null +++ b/tests/Unit/Bulk/Delete/FireEventsTest.php @@ -0,0 +1,372 @@ + $model + * @param string $forceDeleteEvent + * @param string $deleteEvent + * @param string $deleteManyEvent + * @param string $forceDeleteManyEvent + * + * @return void + * + * @throws BulkBindingResolution + * @throws BulkException + * + * @dataProvider modelWithSoftDeletingDataProvider + */ + public function testFiringSoftDelete( + string $model, + string $forceDeleteEvent, + string $deleteEvent, + string $deleteManyEvent, + string $forceDeleteManyEvent, + ): void { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query()->bulk(); + + $forceDeletingCallback = Mockery::spy(TestCallback::class); + $deletingCallback = Mockery::spy(TestCallback::class); + $deletingManyCallback = Mockery::spy(TestCallback::class); + $forceDeletingManyCallback = Mockery::spy(TestCallback::class); + + $model::observe(Observer::class); + Observer::listen($forceDeleteEvent, $forceDeletingCallback); + Observer::listen($deleteEvent, $deletingCallback); + Observer::listen($deleteManyEvent, $deletingManyCallback); + Observer::listen($forceDeleteManyEvent, $forceDeletingManyCallback); + + // act + $sut->delete($users); + + // assert + $modelListenerUserIndex = 0; + + $this->spyShouldHaveReceived($deletingCallback) + ->twice() + ->withArgs( + function (User $user) use ($users, &$modelListenerUserIndex): bool { + if ($user->id === $users->get($modelListenerUserIndex)->id) { + ++$modelListenerUserIndex; + + return $modelListenerUserIndex <= 2; + } + + return false; + } + ); + $this->spyShouldHaveReceived($deletingManyCallback) + ->once() + ->withArgs( + function (UserCollection $actualUsers, BulkRows $bulkRows) use ($users, &$modelListenerUserIndex): bool { + return $actualUsers->count() === $users->count() + && $users->where('id', $actualUsers->get(0)->id)->isNotEmpty() + && $users->where('id', $actualUsers->get(1)->id)->isNotEmpty() + && $actualUsers->get(0)->id !== $actualUsers->get(1)->id + && $bulkRows->count() === $users->count() + && $bulkRows->get(0)->original === $users->get(0) + && $bulkRows->get(1)->original === $users->get(1) + && $bulkRows->get(0)->model === $actualUsers->get(0) + && $bulkRows->get(1)->model === $actualUsers->get(1) + && $bulkRows->get(0)->unique === ['id'] + && $bulkRows->get(1)->unique === ['id'] + && $modelListenerUserIndex === 2; + } + ); + $this->spyShouldNotHaveReceived($forceDeletingCallback); + $this->spyShouldNotHaveReceived($forceDeletingManyCallback); + } + + /** + * @param class-string $model + * @param string $forceDeleteEvent + * @param string $deleteEvent + * @param string $deleteManyEvent + * @param string $forceDeleteManyEvent + * + * @return void + * + * @throws BulkBindingResolution + * @throws BulkException + * + * @dataProvider modelWithSoftDeletingDataProvider + */ + public function testFiringForceDeleting( + string $model, + string $forceDeleteEvent, + string $deleteEvent, + string $deleteManyEvent, + string $forceDeleteManyEvent, + ): void { + // arrange + $users = $this->userGenerator + ->setModel($model) + ->createCollection(2); + $sut = $model::query()->bulk(); + + $forceDeletingListener = Mockery::spy(TestCallback::class); + $deletingListener = Mockery::spy(TestCallback::class); + $deletingManyListener = Mockery::spy(TestCallback::class); + $forceDeletingManyListener = Mockery::spy(TestCallback::class); + + $model::observe(Observer::class); + Observer::listen($forceDeleteEvent, $forceDeletingListener); + Observer::listen($deleteEvent, $deletingListener); + Observer::listen($deleteManyEvent, $deletingManyListener); + Observer::listen($forceDeleteManyEvent, $forceDeletingManyListener); + + // act + $sut->forceDelete($users); + + // assert + $modelListenerUserIndex = 0; + + $this->spyShouldHaveReceived($forceDeletingListener) + ->twice() + ->withArgs( + function (User $user) use ($users, &$modelListenerUserIndex): bool { + if ($user->id === $users->get($modelListenerUserIndex)->id) { + ++$modelListenerUserIndex; + + return $modelListenerUserIndex <= 2; + } + + return false; + } + ); + + $this->spyShouldHaveReceived($deletingListener) + ->twice() + ->withArgs( + function (User $user) use ($users, &$modelListenerUserIndex): bool { + if ($user->id === $users->get($modelListenerUserIndex - 2)->id) { + ++$modelListenerUserIndex; + + return $modelListenerUserIndex >= 3 && $modelListenerUserIndex <= 4; + } + + return false; + } + ); + + $this->spyShouldHaveReceived($deletingManyListener) + ->once() + ->withArgs( + function (UserCollection $actualUsers, BulkRows $bulkRows) use ($users, &$modelListenerUserIndex): bool { + if ($modelListenerUserIndex === 4) { + ++$modelListenerUserIndex; + } else { + return false; + } + + return $actualUsers->count() === $users->count() + && $users->where('id', $actualUsers->get(0)->id)->isNotEmpty() + && $users->where('id', $actualUsers->get(1)->id)->isNotEmpty() + && $actualUsers->get(0)->id !== $actualUsers->get(1)->id + && $bulkRows->count() === $users->count() + && $bulkRows->get(0)->original === $users->get(0) + && $bulkRows->get(1)->original === $users->get(1) + && $bulkRows->get(0)->model === $actualUsers->get(0) + && $bulkRows->get(1)->model === $actualUsers->get(1) + && $bulkRows->get(0)->unique === ['id'] + && $bulkRows->get(1)->unique === ['id']; + } + ); + + $this->spyShouldHaveReceived($forceDeletingManyListener) + ->once() + ->withArgs( + function (UserCollection $actualUsers, BulkRows $bulkRows) use ($users, &$modelListenerUserIndex): bool { + return $actualUsers->count() === $users->count() + && $users->where('id', $actualUsers->get(0)->id)->isNotEmpty() + && $users->where('id', $actualUsers->get(1)->id)->isNotEmpty() + && $actualUsers->get(0)->id !== $actualUsers->get(1)->id + && $bulkRows->count() === $users->count() + && $bulkRows->get(0)->original === $users->get(0) + && $bulkRows->get(1)->original === $users->get(1) + && $bulkRows->get(0)->model === $actualUsers->get(0) + && $bulkRows->get(1)->model === $actualUsers->get(1) + && $bulkRows->get(0)->unique === ['id'] + && $bulkRows->get(1)->unique === ['id'] + && $modelListenerUserIndex === 5; + } + ); + } + + /** + * @param class-string $model + * @param string $forceDeleteEvent + * @param string $deleteEvent + * @param string $deleteManyEvent + * @param string $forceDeleteManyEvent + * + * @return void + * + * @throws BulkBindingResolution + * @throws BulkException + * + * @dataProvider modelWithoutSoftDeletingDataProvider + */ + public function testFiringDeletingWithoutSoft( + string $model, + string $forceDeleteEvent, + string $deleteEvent, + string $deleteManyEvent, + string $forceDeleteManyEvent, + ): void { + // arrange + /** @var PostCollection $posts */ + $posts = $model::factory()->count(2)->create(); + $sut = $model::query()->bulk(); + + $forceDeletingListener = Mockery::spy(TestCallback::class); + $deletingListener = Mockery::spy(TestCallback::class); + $deletingManyListener = Mockery::spy(TestCallback::class); + $forceDeletingManyListener = Mockery::spy(TestCallback::class); + + $model::observe(Observer::class); + Observer::listen($forceDeleteEvent, $forceDeletingListener); + Observer::listen($deleteEvent, $deletingListener); + Observer::listen($deleteManyEvent, $deletingManyListener); + Observer::listen($forceDeleteManyEvent, $forceDeletingManyListener); + + // act + $sut->delete($posts); + + // assert + $modelListenerPostIndex = 0; + + $this->spyShouldHaveReceived($deletingListener) + ->twice() + ->withArgs( + function (Post $post) use ($posts, &$modelListenerPostIndex): bool { + if ($post->id === $posts->get($modelListenerPostIndex)->id) { + ++$modelListenerPostIndex; + + return $modelListenerPostIndex <= 2; + } + + return false; + } + ); + + $this->spyShouldHaveReceived($deletingManyListener) + ->once() + ->withArgs( + function (PostCollection $actualPosts, BulkRows $bulkRows) use ($posts, &$modelListenerPostIndex): bool { + return $actualPosts->count() === $posts->count() + && $posts->where('id', $actualPosts->get(0)->id)->isNotEmpty() + && $posts->where('id', $actualPosts->get(1)->id)->isNotEmpty() + && $actualPosts->get(0)->id !== $actualPosts->get(1)->id + && $bulkRows->count() === $posts->count() + && $bulkRows->get(0)->original === $posts->get(0) + && $bulkRows->get(1)->original === $posts->get(1) + && $bulkRows->get(0)->model === $actualPosts->get(0) + && $bulkRows->get(1)->model === $actualPosts->get(1) + && $bulkRows->get(0)->unique === ['id'] + && $bulkRows->get(1)->unique === ['id'] + && $modelListenerPostIndex === 2; + } + ); + + $this->spyShouldNotHaveReceived($forceDeletingListener); + $this->spyShouldNotHaveReceived($forceDeletingManyListener); + } + + public function modelWithSoftDeletingDataProvider(): array + { + return [ + 'mysql + -ing events' => [ + MySqlUser::class, + BulkEventEnum::FORCE_DELETING, + BulkEventEnum::DELETING, + BulkEventEnum::DELETING_MANY, + BulkEventEnum::FORCE_DELETING_MANY, + ], + 'mysql + -ed events' => [ + MySqlUser::class, + BulkEventEnum::FORCE_DELETED, + BulkEventEnum::DELETED, + BulkEventEnum::DELETED_MANY, + BulkEventEnum::FORCE_DELETED_MANY, + ], + 'postgresql + -ing events' => [ + PostgreSqlUser::class, + BulkEventEnum::FORCE_DELETING, + BulkEventEnum::DELETING, + BulkEventEnum::DELETING_MANY, + BulkEventEnum::FORCE_DELETING_MANY, + ], + 'postgresql + -ed events' => [ + PostgreSqlUser::class, + BulkEventEnum::FORCE_DELETED, + BulkEventEnum::DELETED, + BulkEventEnum::DELETED_MANY, + BulkEventEnum::FORCE_DELETED_MANY, + ], + ]; + } + + public function modelWithoutSoftDeletingDataProvider(): array + { + return [ + 'mysql + -ing events' => [ + MySqlPost::class, + BulkEventEnum::FORCE_DELETING, + BulkEventEnum::DELETING, + BulkEventEnum::DELETING_MANY, + BulkEventEnum::FORCE_DELETING_MANY, + ], + 'mysql + -ed events' => [ + MySqlPost::class, + BulkEventEnum::FORCE_DELETED, + BulkEventEnum::DELETED, + BulkEventEnum::DELETED_MANY, + BulkEventEnum::FORCE_DELETED_MANY, + ], + 'postgresql + -ing events' => [ + PostgreSqlPost::class, + BulkEventEnum::FORCE_DELETING, + BulkEventEnum::DELETING, + BulkEventEnum::DELETING_MANY, + BulkEventEnum::FORCE_DELETING_MANY, + ], + 'postgresql + -ed events' => [ + PostgreSqlPost::class, + BulkEventEnum::FORCE_DELETED, + BulkEventEnum::DELETED, + BulkEventEnum::DELETED_MANY, + BulkEventEnum::FORCE_DELETED_MANY, + ], + ]; + } +} diff --git a/tests/Unit/Bulk/Update/UpdateAfterWritingEventsTest.php b/tests/Unit/Bulk/Update/UpdateAfterWritingEventsTest.php index 6e7e0cd..5f1d598 100644 --- a/tests/Unit/Bulk/Update/UpdateAfterWritingEventsTest.php +++ b/tests/Unit/Bulk/Update/UpdateAfterWritingEventsTest.php @@ -13,7 +13,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\App\Support\TestCallback; use Lapaliv\BulkUpsert\Tests\TestCase; use Mockery; @@ -43,13 +43,13 @@ public function testModel(string $model, Closure $data, array $events): void $sut = $model::query() ->bulk() ->uniqueBy(['email']); - $model::observe(UserObserver::class); + $model::observe(Observer::class); $spies = []; foreach ($events as $event) { $spies[$event] = Mockery::spy(TestCallback::class, $event); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); } // act @@ -93,8 +93,8 @@ public function testCollection(string $model, Closure $data, string $event): voi ->bulk() ->uniqueBy(['email']); $spy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listen($event, $spy); + $model::observe(Observer::class); + Observer::listen($event, $spy); // act $sut->update($users); diff --git a/tests/Unit/Bulk/Update/UpdateBeforeWritingEventDependenciesTest.php b/tests/Unit/Bulk/Update/UpdateBeforeWritingEventDependenciesTest.php index fa48123..39ec5ff 100644 --- a/tests/Unit/Bulk/Update/UpdateBeforeWritingEventDependenciesTest.php +++ b/tests/Unit/Bulk/Update/UpdateBeforeWritingEventDependenciesTest.php @@ -13,7 +13,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\App\Support\TestCallback; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -50,7 +50,7 @@ public function testModelEventReturnsFalseSometimes( ): void { // arrange $users = $data(); - $model::observe(UserObserver::class); + $model::observe(Observer::class); /** @var array $spies */ $spies = [ $event => Mockery::mock(TestCallback::class), @@ -59,12 +59,12 @@ public function testModelEventReturnsFalseSometimes( ->expects('__invoke') ->times(count($users)) ->andReturnValues([false, true]); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); foreach ($dependencies as $dependencyList) { foreach ($dependencyList as $dependency) { $spies[$dependency] = Mockery::spy(TestCallback::class, $dependency); - UserObserver::listen($dependency, $spies[$dependency]); + Observer::listen($dependency, $spies[$dependency]); } } @@ -124,7 +124,7 @@ public function testModelEventReturnsFalseAlways( ): void { // arrange $users = $data(); - $model::observe(UserObserver::class); + $model::observe(Observer::class); /** @var array $spies */ $spies = [ $event => Mockery::mock(TestCallback::class), @@ -133,12 +133,12 @@ public function testModelEventReturnsFalseAlways( ->expects('__invoke') ->times(count($users)) ->andReturnFalse(); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); foreach ($dependencies as $dependencyList) { foreach ($dependencyList as $dependency) { $spies[$dependency] = Mockery::spy(TestCallback::class, $dependency); - UserObserver::listen($dependency, $spies[$dependency]); + Observer::listen($dependency, $spies[$dependency]); } } @@ -182,7 +182,7 @@ public function testCollectionEventReturnsFalse( ): void { // arrange $users = $data(); - $model::observe(UserObserver::class); + $model::observe(Observer::class); /** @var array $spies */ $spies = [ $event => Mockery::mock(TestCallback::class), @@ -191,12 +191,12 @@ public function testCollectionEventReturnsFalse( ->expects('__invoke') ->once() ->andReturnFalse(); - UserObserver::listen($event, $spies[$event]); + Observer::listen($event, $spies[$event]); foreach ($dependencies as $dependencyList) { foreach ($dependencyList as $dependency) { $spies[$dependency] = Mockery::spy(TestCallback::class, $dependency); - UserObserver::listen($dependency, $spies[$dependency]); + Observer::listen($dependency, $spies[$dependency]); } } @@ -231,16 +231,16 @@ public function testDoNotFireUpdatingIfTheModelIsNotDirty(string $model): void { // arrange $users = $this->userGenerator->createCollection(2); - $model::observe(UserObserver::class); + $model::observe(Observer::class); $savingSpy = Mockery::spy(TestCallback::class); $savingManySpy = Mockery::spy(TestCallback::class); $updatingSpy = Mockery::spy(TestCallback::class); $updatingManySpy = Mockery::spy(TestCallback::class); - UserObserver::listen(BulkEventEnum::SAVING, $savingSpy); - UserObserver::listen(BulkEventEnum::SAVING_MANY, $savingManySpy); - UserObserver::listen(BulkEventEnum::UPDATING, $updatingSpy); - UserObserver::listen(BulkEventEnum::UPDATING_MANY, $updatingManySpy); + Observer::listen(BulkEventEnum::SAVING, $savingSpy); + Observer::listen(BulkEventEnum::SAVING_MANY, $savingManySpy); + Observer::listen(BulkEventEnum::UPDATING, $updatingSpy); + Observer::listen(BulkEventEnum::UPDATING_MANY, $updatingManySpy); $sut = $model::query() ->bulk() diff --git a/tests/Unit/Bulk/Update/UpdateEventsTest.php b/tests/Unit/Bulk/Update/UpdateEventsTest.php index eca6506..8b28748 100644 --- a/tests/Unit/Bulk/Update/UpdateEventsTest.php +++ b/tests/Unit/Bulk/Update/UpdateEventsTest.php @@ -7,7 +7,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\App\Support\TestCallback; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -40,8 +40,8 @@ public function testDisableAllEvents(string $model): void ->disableEvents(); $spy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($spy); + $model::observe(Observer::class); + Observer::listenAny($spy); // act $sut->update($users); @@ -73,11 +73,11 @@ public function testDisableModelEndEvents(string $model): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); foreach ($disabledEvents as $event) { - UserObserver::listen($event, $notCallingSpy); + Observer::listen($event, $notCallingSpy); } // act @@ -113,9 +113,9 @@ public function testDisableSomeEvents(string $model, string $disabledEvent): voi $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); - UserObserver::listen($disabledEvent, $notCallingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); + Observer::listen($disabledEvent, $notCallingSpy); // act $sut->update($users); @@ -150,9 +150,9 @@ public function testDisableOneEvent(string $model, string $disabledEvent): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($callingSpy); - UserObserver::listen($disabledEvent, $notCallingSpy); + $model::observe(Observer::class); + Observer::listenAny($callingSpy); + Observer::listen($disabledEvent, $notCallingSpy); // act $sut->update($users); @@ -184,8 +184,8 @@ public function testEnableAllEvents(string $model): void ->enableEvents(); $spy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($spy); + $model::observe(Observer::class); + Observer::listenAny($spy); $countOfCallingPerModel = count(BulkEventEnum::update()) + count(BulkEventEnum::save()); // act @@ -224,8 +224,8 @@ public function testEnableSomeDisabledEvents(string $model, string $enabledEvent $callingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listen($enabledEvent, $callingSpy); + $model::observe(Observer::class); + Observer::listen($enabledEvent, $callingSpy); // act $sut->update($users); @@ -258,9 +258,9 @@ public function testEnableSomeEvents(string $model, string $enabledEvent): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($notCallingSpy, [$enabledEvent]); - UserObserver::listen($enabledEvent, $callingSpy); + $model::observe(Observer::class); + Observer::listenAny($notCallingSpy, [$enabledEvent]); + Observer::listen($enabledEvent, $callingSpy); // act $sut->update($users); @@ -294,9 +294,9 @@ public function testEnableOneEvent(string $model, string $enabledEvent): void $callingSpy = Mockery::spy(TestCallback::class); $notCallingSpy = Mockery::spy(TestCallback::class); - $model::observe(UserObserver::class); - UserObserver::listenAny($notCallingSpy, [$enabledEvent]); - UserObserver::listen($enabledEvent, $callingSpy); + $model::observe(Observer::class); + Observer::listenAny($notCallingSpy, [$enabledEvent]); + Observer::listen($enabledEvent, $callingSpy); // act $sut->update($users); diff --git a/tests/Unit/BulkBuilderTrait/SelectAndUpdateManyTest.php b/tests/Unit/BulkBuilderTrait/SelectAndUpdateManyTest.php index 59fb1cd..f477b32 100644 --- a/tests/Unit/BulkBuilderTrait/SelectAndUpdateManyTest.php +++ b/tests/Unit/BulkBuilderTrait/SelectAndUpdateManyTest.php @@ -6,7 +6,7 @@ use Lapaliv\BulkUpsert\Tests\App\Models\MySqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\PostgreSqlUser; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; use Lapaliv\BulkUpsert\Tests\TestCase; use Lapaliv\BulkUpsert\Tests\Unit\UserTestTrait; @@ -98,7 +98,7 @@ public function testWithEventsById(string $model): void 'phones' => $fakeUser->phones, 'last_visited_at' => $fakeUser->last_visited_at, ]; - $model::observe(UserObserver::class); + $model::observe(Observer::class); $sut = $model::query() ->whereIn('email', $users->pluck('email')); @@ -154,7 +154,7 @@ public function testWithEventsByEmail(string $model): void 'phones' => $fakeUser->phones, 'last_visited_at' => $fakeUser->last_visited_at, ]; - $model::observe(UserObserver::class); + $model::observe(Observer::class); $sut = $model::query() ->whereIn('email', $users->pluck('email')); diff --git a/tests/Unit/UserTestTrait.php b/tests/Unit/UserTestTrait.php index e2f2a02..8c88235 100644 --- a/tests/Unit/UserTestTrait.php +++ b/tests/Unit/UserTestTrait.php @@ -5,7 +5,7 @@ use Carbon\Carbon; use Lapaliv\BulkUpsert\Tests\App\Features\UserGenerator; use Lapaliv\BulkUpsert\Tests\App\Models\User; -use Lapaliv\BulkUpsert\Tests\App\Observers\UserObserver; +use Lapaliv\BulkUpsert\Tests\App\Observers\Observer; /** * @internal @@ -20,7 +20,7 @@ public function setUp(): void $this->userGenerator = new UserGenerator(); Carbon::setTestNow(Carbon::now()); - UserObserver::flush(); + Observer::flush(); } protected function userWasCreated(User $user): void @@ -42,6 +42,55 @@ protected function userWasCreated(User $user): void ], $user->getConnectionName()); } + protected function userExists(User $user): void + { + self::assertDatabaseHas($user->getTable(), [ + 'email' => $user->email, + 'name' => $user->name, + 'gender' => $user->gender->value, + 'avatar' => $user->avatar, + 'posts_count' => $user->posts_count, + 'is_admin' => $user->is_admin, + 'balance' => $user->balance, + 'birthday' => $user->birthday?->toDateString(), + 'phones' => $user->phones, + 'last_visited_at' => $user->last_visited_at, + 'created_at' => $user->created_at->toDateTimeString(), + 'updated_at' => $user->updated_at->toDateTimeString(), + 'deleted_at' => $user->deleted_at?->toDateTimeString(), + ], $user->getConnectionName()); + } + + protected function userDoesNotExist(User $user): void + { + self::assertDatabaseMissing($user->getTable(), [ + 'email' => $user->email, + ], $user->getConnectionName()); + } + + protected function userWasSoftDeleted(User $user): void + { + self::assertDatabaseHas($user->getTable(), [ + 'email' => $user->email, + 'name' => $user->name, + 'gender' => $user->gender->value, + 'avatar' => $user->avatar, + 'posts_count' => $user->posts_count, + 'is_admin' => $user->is_admin, + 'balance' => $user->balance, + 'birthday' => $user->birthday?->toDateString(), + 'phones' => $user->phones, + 'last_visited_at' => $user->last_visited_at, + 'created_at' => $user->created_at->toDateTimeString(), + 'updated_at' => $user->updated_at->toDateTimeString(), + ], $user->getConnectionName()); + + self::assertDatabaseMissing($user->getTable(), [ + 'email' => $user->email, + 'deleted_at' => null, + ], $user->getConnectionName()); + } + protected function userWasUpdated(User $user): void { self::assertDatabaseHas($user->getTable(), [ diff --git a/tests/app/Factories/PostFactory.php b/tests/app/Factories/PostFactory.php index 3895884..69e5ede 100644 --- a/tests/app/Factories/PostFactory.php +++ b/tests/app/Factories/PostFactory.php @@ -3,9 +3,17 @@ namespace Lapaliv\BulkUpsert\Tests\App\Factories; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Model; +use Lapaliv\BulkUpsert\Tests\App\Collection\PostCollection; +use Lapaliv\BulkUpsert\Tests\App\Models\Post; /** * @internal + * + * @method Post|PostCollection create($attributes = [], ?Model $parent = null) + * @method Post|PostCollection make($attributes = [], ?Model $parent = null) + * @method Post|PostCollection createMany(iterable $records) + * @method self|static count(?int $count) */ abstract class PostFactory extends Factory { diff --git a/tests/app/Models/Post.php b/tests/app/Models/Post.php index 15a374b..9f7a651 100644 --- a/tests/app/Models/Post.php +++ b/tests/app/Models/Post.php @@ -9,6 +9,7 @@ use Lapaliv\BulkUpsert\Tests\App\Builders\CommentBuilder; use Lapaliv\BulkUpsert\Tests\App\Builders\PostBuilder; use Lapaliv\BulkUpsert\Tests\App\Collection\PostCollection; +use Lapaliv\BulkUpsert\Tests\App\Factories\PostFactory; use Lapaliv\BulkUpsert\Tests\App\Traits\GlobalTouches; /** @@ -22,6 +23,7 @@ * @property-read Comment $comment * * @method static PostBuilder query() + * @method static PostFactory factory($count = null, $state = []) */ abstract class Post extends Model { diff --git a/tests/app/Observers/UserObserver.php b/tests/app/Observers/Observer.php similarity index 84% rename from tests/app/Observers/UserObserver.php rename to tests/app/Observers/Observer.php index 742bc04..076c0d6 100644 --- a/tests/app/Observers/UserObserver.php +++ b/tests/app/Observers/Observer.php @@ -8,7 +8,7 @@ /** * @internal */ -final class UserObserver +final class Observer { public static array $listeners = []; @@ -175,6 +175,42 @@ public function deletedMany() return null; } + public function forceDeleting() + { + if (array_key_exists(__FUNCTION__, self::$listeners)) { + return self::$listeners[__FUNCTION__](...func_get_args()); + } + + return null; + } + + public function forceDeletingMany() + { + if (array_key_exists(__FUNCTION__, self::$listeners)) { + return self::$listeners[__FUNCTION__](...func_get_args()); + } + + return null; + } + + public function forceDeleted() + { + if (array_key_exists(__FUNCTION__, self::$listeners)) { + return self::$listeners[__FUNCTION__](...func_get_args()); + } + + return null; + } + + public function forceDeletedMany() + { + if (array_key_exists(__FUNCTION__, self::$listeners)) { + return self::$listeners[__FUNCTION__](...func_get_args()); + } + + return null; + } + public function restoring() { if (array_key_exists(__FUNCTION__, self::$listeners)) {