Skip to content

Commit 760cf51

Browse files
authored
Merge pull request #576 from wayofdev/feat/soft-deletes
2 parents e8c4d38 + dc1b6b5 commit 760cf51

21 files changed

+918
-281
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"phpunit/phpunit": "^10.5",
4949
"roave/security-advisories": "dev-latest",
5050
"spatie/laravel-ray": "^1.32",
51-
"wayofdev/cs-fixer-config": "^1.1"
51+
"wayofdev/cs-fixer-config": "^1.1",
52+
"beberlei/assert": "^3.3"
5253
},
5354
"autoload": {
5455
"psr-4": {

composer.lock

Lines changed: 321 additions & 262 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Bridge/Laravel/Console/Commands/Migrations/AbstractCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected function verifyConfigured(): bool
3636
return true;
3737
}
3838

39-
protected function verifyEnvironment(string $confirmationQuestion = null): bool
39+
protected function verifyEnvironment(?string $confirmationQuestion = null): bool
4040
{
4141
$confirmationQuestion = $confirmationQuestion ?? self::DEFAULT_CONFIRMATION;
4242

src/Bridge/Laravel/LoggerFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function __construct(private readonly Logger $logger)
1515
{
1616
}
1717

18-
public function getLogger(DriverInterface $driver = null): LoggerInterface
18+
public function getLogger(?DriverInterface $driver = null): LoggerInterface
1919
{
2020
return $this->logger->getLogger();
2121
}

src/Testing/Concerns/InteractsWithDatabase.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use PHPUnit\Framework\Constraint\LogicalNot as ReverseConstraint;
1111
use WayOfDev\Cycle\Testing\Constraints\CountInDatabase;
1212
use WayOfDev\Cycle\Testing\Constraints\HasInDatabase;
13+
use WayOfDev\Cycle\Testing\Constraints\NotSoftDeletedInDatabase;
14+
use WayOfDev\Cycle\Testing\Constraints\SoftDeletedInDatabase;
1315

1416
/**
1517
* @method void assertThat($value, Constraint $constraint, string $message = '')
@@ -88,4 +90,32 @@ protected function cleanupMigrations(string $pathGlob): void
8890
File::delete($file);
8991
}
9092
}
93+
94+
protected function assertSoftDeleted($table, array $data = [], $connection = null, $deletedAtColumn = 'deleted_at'): self
95+
{
96+
$this->assertThat(
97+
$table,
98+
new SoftDeletedInDatabase(
99+
app(DatabaseProviderInterface::class),
100+
$data,
101+
$deletedAtColumn,
102+
)
103+
);
104+
105+
return $this;
106+
}
107+
108+
protected function assertNotSoftDeleted($table, array $data = [], $connection = null, $deletedAtColumn = 'deleted_at'): self
109+
{
110+
$this->assertThat(
111+
$table,
112+
new NotSoftDeletedInDatabase(
113+
app(DatabaseProviderInterface::class),
114+
$data,
115+
$deletedAtColumn,
116+
)
117+
);
118+
119+
return $this;
120+
}
91121
}

src/Testing/Constraints/HasInDatabase.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Cycle\Database\DatabaseInterface;
88
use Cycle\Database\DatabaseProviderInterface;
99
use Cycle\Database\Query\SelectQuery;
10-
use JsonException;
1110
use PHPUnit\Framework\Constraint\Constraint;
1211
use Throwable;
1312

@@ -44,9 +43,6 @@ public function matches(mixed $other): bool
4443
}
4544
}
4645

47-
/**
48-
* @throws JsonException
49-
*/
5046
public function failureDescription(mixed $other): string
5147
{
5248
return sprintf(
@@ -56,12 +52,7 @@ public function failureDescription(mixed $other): string
5652
);
5753
}
5854

59-
/**
60-
* @param mixed|null $options
61-
*
62-
* @throws JsonException
63-
*/
64-
public function toString($options = null): string
55+
public function toString(mixed $options = null): string
6556
{
6657
if (is_int($options)) {
6758
$options |= JSON_THROW_ON_ERROR;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WayOfDev\Cycle\Testing\Constraints;
6+
7+
use Cycle\Database\DatabaseInterface;
8+
use Cycle\Database\DatabaseProviderInterface;
9+
use Cycle\Database\Query\SelectQuery;
10+
use PHPUnit\Framework\Constraint\Constraint;
11+
use Throwable;
12+
13+
use function json_encode;
14+
use function sprintf;
15+
16+
class NotSoftDeletedInDatabase extends Constraint
17+
{
18+
protected int $show = 3;
19+
20+
protected DatabaseInterface $database;
21+
22+
protected array $data;
23+
24+
protected string $deletedAtColumn;
25+
26+
public function __construct(DatabaseProviderInterface $database, array $data, string $deletedAtColumn)
27+
{
28+
$this->data = $data;
29+
30+
$this->database = $database->database();
31+
32+
$this->deletedAtColumn = $deletedAtColumn;
33+
}
34+
35+
public function matches(mixed $other): bool
36+
{
37+
/** @var SelectQuery $tableInterface */
38+
$tableInterface = $this->database->table($other);
39+
40+
try {
41+
$count = $tableInterface->where($this->data)
42+
->andWhere($this->deletedAtColumn, '=', null)
43+
->count();
44+
45+
return 0 < $count;
46+
} catch (Throwable $e) {
47+
return false;
48+
}
49+
}
50+
51+
public function failureDescription($other): string
52+
{
53+
return sprintf(
54+
'any existing row in the table [%s] matches the attributes %s.\n',
55+
$other,
56+
$this->toString()
57+
);
58+
}
59+
60+
public function toString(): string
61+
{
62+
return json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
63+
}
64+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WayOfDev\Cycle\Testing\Constraints;
6+
7+
use Cycle\Database\DatabaseInterface;
8+
use Cycle\Database\DatabaseProviderInterface;
9+
use Cycle\Database\Query\SelectQuery;
10+
use PHPUnit\Framework\Constraint\Constraint;
11+
use Throwable;
12+
13+
use function json_encode;
14+
use function sprintf;
15+
16+
class SoftDeletedInDatabase extends Constraint
17+
{
18+
protected int $show = 3;
19+
20+
protected DatabaseInterface $database;
21+
22+
protected array $data;
23+
24+
protected string $deletedAtColumn;
25+
26+
public function __construct(DatabaseProviderInterface $database, array $data, string $deletedAtColumn)
27+
{
28+
$this->data = $data;
29+
30+
$this->database = $database->database();
31+
32+
$this->deletedAtColumn = $deletedAtColumn;
33+
}
34+
35+
public function matches(mixed $other): bool
36+
{
37+
/** @var SelectQuery $tableInterface */
38+
$tableInterface = $this->database->table($other);
39+
40+
try {
41+
$count = $tableInterface->where($this->data)
42+
->andWhere($this->deletedAtColumn, '!=', null)
43+
->count();
44+
45+
return 0 < $count;
46+
} catch (Throwable $e) {
47+
return false;
48+
}
49+
}
50+
51+
public function failureDescription($other): string
52+
{
53+
return sprintf(
54+
'a soft deleted row in the table [%s] matches the attributes %s.',
55+
$other,
56+
$this->toString()
57+
);
58+
}
59+
60+
public function toString(): string
61+
{
62+
return json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
63+
}
64+
}

tests/app/Entities/Footprint.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WayOfDev\App\Entities;
6+
7+
use Cycle\Database\DatabaseInterface;
8+
use JsonException;
9+
use JsonSerializable;
10+
use Ramsey\Uuid\Uuid;
11+
use Stringable;
12+
13+
use function json_decode;
14+
use function json_encode;
15+
16+
final class Footprint implements JsonSerializable, Stringable
17+
{
18+
private UserId $id;
19+
20+
private string $party;
21+
22+
private string $realm;
23+
24+
public static function empty(string $authorizedParty = 'guest-party', string $realm = 'guest-realm'): self
25+
{
26+
return new self(UserId::create(Uuid::NIL), $authorizedParty, $realm);
27+
}
28+
29+
public static function random(string $authorizedParty = 'random-party', string $realm = 'random-realm'): self
30+
{
31+
return new self(UserId::create(Uuid::uuid7()->toString()), $authorizedParty, $realm);
32+
}
33+
34+
public static function fromArray(array $data): self
35+
{
36+
$userId = UserId::fromString($data['id']);
37+
38+
return new self($userId, $data['party'], $data['realm']);
39+
}
40+
41+
/**
42+
* https://cycle-orm.dev/docs/advanced-column-wrappers/2.x/en.
43+
*
44+
* @throws JsonException
45+
*/
46+
public static function castValue(string $value, DatabaseInterface $db): self
47+
{
48+
return self::fromArray(
49+
json_decode($value, true, 512, JSON_THROW_ON_ERROR)
50+
);
51+
}
52+
53+
public function toArray(): array
54+
{
55+
return [
56+
'id' => $this->id->toString(),
57+
'party' => $this->party,
58+
'realm' => $this->realm,
59+
];
60+
}
61+
62+
public function id(): UserId
63+
{
64+
return $this->id;
65+
}
66+
67+
public function party(): string
68+
{
69+
return $this->party;
70+
}
71+
72+
public function realm(): string
73+
{
74+
return $this->realm;
75+
}
76+
77+
public function jsonSerialize(): array
78+
{
79+
return $this->toArray();
80+
}
81+
82+
/**
83+
* @throws JsonException
84+
*/
85+
public function __toString(): string
86+
{
87+
return json_encode($this->toArray(), JSON_THROW_ON_ERROR);
88+
}
89+
90+
private function __construct(UserId $id, string $party, string $realm)
91+
{
92+
$this->id = $id;
93+
$this->party = $party;
94+
$this->realm = $realm;
95+
}
96+
}

tests/app/Entities/HasSignatures.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WayOfDev\App\Entities;
6+
7+
use Cycle\Annotated\Annotation\Relation\Embedded;
8+
9+
trait HasSignatures
10+
{
11+
#[Embedded(target: Signature::class, prefix: 'created_')]
12+
private Signature $created;
13+
14+
#[Embedded(target: Signature::class, prefix: 'updated_')]
15+
private Signature $updated;
16+
17+
#[Embedded(target: Signature::class, prefix: 'deleted_')]
18+
private ?Signature $deleted;
19+
20+
public function created(): Signature
21+
{
22+
return $this->created;
23+
}
24+
25+
public function updated(): Signature
26+
{
27+
return $this->updated;
28+
}
29+
30+
public function deleted(): ?Signature
31+
{
32+
if (! $this->deleted?->defined()) {
33+
return null;
34+
}
35+
36+
return $this->deleted;
37+
}
38+
39+
public function softDelete(Signature $deleted): void
40+
{
41+
$this->deleted = $deleted;
42+
}
43+
}

0 commit comments

Comments
 (0)