feat: 🎉 base DDD project
This commit is contained in:
parent
06cdc3e7ca
commit
27cdac86cb
33 changed files with 6879 additions and 0 deletions
19
.editorconfig
Normal file
19
.editorconfig
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,json}]
|
||||
indent_size = 4
|
||||
|
||||
[*.{css,scss}]
|
||||
indent_size = 2
|
||||
quote_type = single
|
||||
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
.env
|
||||
.env.testing
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
.stylelintcache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/node_modules
|
||||
/bootstrap/cache
|
||||
/public/build
|
||||
/public/cache
|
||||
/public/hot
|
||||
/public/sitemap.xml
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
coverage/
|
||||
auth.json
|
||||
docker-compose.override.yml
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.npmrc
|
||||
js.save
|
||||
phpstan-report.xml
|
||||
|
||||
# Ignore public favicon
|
||||
/public/android-chrome-192x192.png
|
||||
/public/android-chrome-512x512.png
|
||||
/public/apple-touch-icon.png
|
||||
/public/browserconfig.xml
|
||||
/public/browserconfig.xml
|
||||
/public/favicon-16x16.png
|
||||
/public/favicon-32x32.png
|
||||
/public/favicon.ico
|
||||
/public/mstile-150x150.png
|
||||
/public/safari-pinned-tab.svg
|
||||
/public/site.webmanifest
|
||||
/public/files
|
||||
|
||||
develop
|
||||
_ide_helper.php
|
||||
.phpstorm.meta.php
|
||||
phpstan-report.xml
|
||||
|
||||
// Ignore PHPUnit cache files
|
||||
cache
|
||||
64
composer.json
Normal file
64
composer.json
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "projets/mesbr",
|
||||
"type": "project",
|
||||
"description": "MesBr project.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"tests"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"ace-of-aces/intellipest": "^0.4.0",
|
||||
"laravel/pint": "^1.22",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"nunomaduro/mock-final-classes": "^1.2",
|
||||
"pestphp/pest": "^4.0",
|
||||
"pestphp/pest-plugin-faker": "^4.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^4.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
"phpunit/phpunit": "^12.5",
|
||||
"ramsey/uuid": "^4.9",
|
||||
"rector/rector": "^2.5",
|
||||
"slevomat/coding-standard": "^8.22"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"php-http/discovery": true,
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Application\\": "src/Application/",
|
||||
"Domain\\": "src/Domain/",
|
||||
"Shared\\": "src/Shared/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"tests": "vendor/bin/pest",
|
||||
"linters": "vendor/bin/pint && vendor/bin/rector && vendor/bin/phpcs && vendor/bin/phpstan analyse"
|
||||
}
|
||||
}
|
||||
5608
composer.lock
generated
Normal file
5608
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
43
docs/ADR/architecture/0001-architecture.md
Normal file
43
docs/ADR/architecture/0001-architecture.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# ADR-0001 — [Titre]
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
### Quel est le problème ?
|
||||
|
||||
[Réponse]
|
||||
|
||||
### Pourquoi devons-nous prendre cette décision ?
|
||||
|
||||
[Réponse]
|
||||
|
||||
## Decision
|
||||
|
||||
[Qu'avons-nous décidé ?]
|
||||
|
||||
## Rationale
|
||||
|
||||
[Pourquoi cette solution ?]
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positives
|
||||
|
||||
- ...
|
||||
|
||||
### Negatives
|
||||
|
||||
- ...
|
||||
|
||||
## Alternatives
|
||||
|
||||
### Alternative 1
|
||||
|
||||
[Description + pourquoi rejetée]
|
||||
|
||||
### Alternative 2
|
||||
|
||||
[Description + pourquoi rejetée]
|
||||
52
phpcs.xml
Normal file
52
phpcs.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="Php Coding Standard"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
|
||||
|
||||
<description>Standard optimisé: Analyse statique complémentaire à Pint.</description>
|
||||
|
||||
<arg name="colors" />
|
||||
<arg name="extensions" value="php" />
|
||||
<arg name="parallel" value="75" />
|
||||
<file>src/</file>
|
||||
<file>tests/</file>
|
||||
|
||||
<exclude-pattern>*/vendor/*</exclude-pattern>
|
||||
<exclude-pattern>*/storage/*</exclude-pattern>
|
||||
<exclude-pattern>_ide_helper.php</exclude-pattern>
|
||||
|
||||
<rule ref="PSR12"/>
|
||||
|
||||
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
|
||||
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
|
||||
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
|
||||
<rule ref="Generic.Metrics.CyclomaticComplexity">
|
||||
<properties>
|
||||
<property name="complexity" value="10"/>
|
||||
<property name="absoluteComplexity" value="20"/>
|
||||
</properties>
|
||||
</rule>
|
||||
<rule ref="Generic.Metrics.NestingLevel"/>
|
||||
|
||||
<rule ref="Generic.PHP.DeprecatedFunctions"/>
|
||||
<rule ref="Generic.PHP.ForbiddenFunctions"/>
|
||||
<rule ref="Generic.PHP.NoSilencedErrors"/>
|
||||
<rule ref="Squiz.PHP.Eval"/>
|
||||
<rule ref="Squiz.PHP.GlobalKeyword"/>
|
||||
<rule ref="Squiz.PHP.NonExecutableCode"/>
|
||||
<rule ref="Squiz.Scope.StaticThisUsage"/>
|
||||
|
||||
<rule ref="Generic.NamingConventions.ConstructorName"/>
|
||||
<rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
|
||||
|
||||
<rule ref="Generic.Files.LineLength">
|
||||
<properties>
|
||||
<property name="lineLimit" value="150"/>
|
||||
<property name="absoluteLineLimit" value="250"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
|
||||
<exclude-pattern>*/database/migrations/*</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
9
phpstan.neon
Normal file
9
phpstan.neon
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
parameters:
|
||||
paths:
|
||||
- src/
|
||||
|
||||
# Level 10 is the highest level
|
||||
level: 10
|
||||
|
||||
# Enforce strict types analysis
|
||||
treatPhpDocTypesAsCertain: false
|
||||
22
phpunit.xml
Normal file
22
phpunit.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Architecture">
|
||||
<directory suffix="Test.php">./tests/Architecture</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</source>
|
||||
<coverage>
|
||||
<report>
|
||||
<clover outputFile="coverage/clover.xml"/>
|
||||
<html outputDirectory="coverage/html"/>
|
||||
</report>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
78
pint.json
Normal file
78
pint.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"preset": "psr12",
|
||||
"rules": {
|
||||
"indentation_type": true,
|
||||
"single_line_empty_body": false,
|
||||
"types_spaces": {
|
||||
"space": "single",
|
||||
"space_multiple_catch": "single"
|
||||
},
|
||||
"control_structure_braces": true,
|
||||
"new_with_parentheses": {
|
||||
"anonymous_class": true,
|
||||
"named_class": true
|
||||
},
|
||||
"class_definition": {
|
||||
"single_item_single_line": false,
|
||||
"single_line": false,
|
||||
"space_before_parenthesis": true
|
||||
},
|
||||
"curly_braces_position": {
|
||||
"allow_single_line_empty_anonymous_classes": false,
|
||||
"classes_opening_brace": "next_line_unless_newline_at_signature_end",
|
||||
"anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end",
|
||||
"control_structures_opening_brace": "same_line",
|
||||
"functions_opening_brace": "next_line_unless_newline_at_signature_end",
|
||||
"anonymous_functions_opening_brace": "same_line"
|
||||
},
|
||||
"binary_operator_spaces": {
|
||||
"default": "align_single_space_minimal",
|
||||
"operators": {
|
||||
"=": "align_single_space",
|
||||
"|": "single_space",
|
||||
"=>": "align_single_space",
|
||||
".=": "align_single_space"
|
||||
}
|
||||
},
|
||||
"concat_space": {
|
||||
"spacing": "one"
|
||||
},
|
||||
"align_multiline_comment": {
|
||||
"comment_type": "all_multiline"
|
||||
},
|
||||
"array_indentation": true,
|
||||
"method_chaining_indentation": true,
|
||||
"statement_indentation": {
|
||||
"stick_comment_to_next_continuous_control_statement": true
|
||||
},
|
||||
"no_unused_imports": true,
|
||||
"fully_qualified_strict_types": {
|
||||
"import_symbols": true,
|
||||
"leading_backslash_in_global_namespace": true,
|
||||
"phpdoc_tags": []
|
||||
},
|
||||
"visibility_required": {
|
||||
"elements": [
|
||||
"const",
|
||||
"method",
|
||||
"property"
|
||||
]
|
||||
},
|
||||
"no_useless_else": true,
|
||||
"function_declaration": {
|
||||
"closure_function_spacing": "one",
|
||||
"closure_fn_spacing": "one",
|
||||
"trailing_comma_single_line": false
|
||||
},
|
||||
"single_line_after_imports": true,
|
||||
"combine_consecutive_issets": true,
|
||||
"array_syntax": {
|
||||
"syntax": "short"
|
||||
},
|
||||
"single_quote": true,
|
||||
"blank_line_after_namespace": true,
|
||||
"blank_line_after_opening_tag": true,
|
||||
"declare_strict_types": true
|
||||
}
|
||||
}
|
||||
21
rector.php
Normal file
21
rector.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
|
||||
return RectorConfig::configure()
|
||||
->withPaths([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
])
|
||||
->withPhpSets()
|
||||
->withSets([
|
||||
SetList::CODE_QUALITY,
|
||||
SetList::DEAD_CODE,
|
||||
SetList::TYPE_DECLARATION,
|
||||
SetList::EARLY_RETURN,
|
||||
SetList::PRIVATIZATION,
|
||||
SetList::INSTANCEOF,
|
||||
]);
|
||||
0
src/Application/.gitkeep
Normal file
0
src/Application/.gitkeep
Normal file
39
src/Domain/Models/Entities/Product.php
Normal file
39
src/Domain/Models/Entities/Product.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\Entities;
|
||||
|
||||
use Domain\Models\ValueObjects\ProductId;
|
||||
use DomainException;
|
||||
use Shared\DDD\Entity;
|
||||
|
||||
/**
|
||||
* @implements \Shared\DDD\Entity<\Domain\Models\ValueObjects\ProductId>
|
||||
*/
|
||||
final readonly class Product implements Entity
|
||||
{
|
||||
public function __construct(
|
||||
private ProductId $productId,
|
||||
private string $name,
|
||||
) {
|
||||
if (trim($name) === '') {
|
||||
throw new DomainException('Product name cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): ProductId
|
||||
{
|
||||
return $this->productId;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function equals(Product $product): bool
|
||||
{
|
||||
return $this->productId->equals($product->productId);
|
||||
}
|
||||
}
|
||||
57
src/Domain/Models/ValueObjects/ProductId.php
Normal file
57
src/Domain/Models/ValueObjects/ProductId.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects;
|
||||
|
||||
use DateTimeInterface;
|
||||
use DomainException;
|
||||
use InvalidArgumentException;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Shared\DDD\EntityId;
|
||||
use Shared\Helpers\UuidHelper;
|
||||
|
||||
final readonly class ProductId extends EntityId
|
||||
{
|
||||
/**
|
||||
* @param UuidInterface $identifier Universally unique identifier.
|
||||
*/
|
||||
public function __construct(
|
||||
private UuidInterface $identifier
|
||||
) {
|
||||
parent::__construct($identifier->toString());
|
||||
|
||||
try {
|
||||
UuidHelper::validateUuid($identifier);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new DomainException(
|
||||
"Invalid value '{$identifier->toString()}' is not a valid UUID",
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
if (! UuidHelper::isV7($identifier)) {
|
||||
throw new DomainException(
|
||||
"Invalid value '{$identifier}' is not a valid V7 UUID"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the value of the universally unique identifier.
|
||||
*/
|
||||
public function toDateTime(): DateTimeInterface
|
||||
{
|
||||
return UuidHelper::toDateTime($this->identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the value of the universally unique identifier.
|
||||
*/
|
||||
public static function generate(): self
|
||||
{
|
||||
return new self(Uuid::uuid7());
|
||||
}
|
||||
}
|
||||
14
src/Shared/DDD/AggregateRoot.php
Normal file
14
src/Shared/DDD/AggregateRoot.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\DDD;
|
||||
|
||||
/**
|
||||
* @template T of EntityId
|
||||
*
|
||||
* @extends Entity<T>
|
||||
*/
|
||||
interface AggregateRoot extends Entity
|
||||
{
|
||||
}
|
||||
14
src/Shared/DDD/Entity.php
Normal file
14
src/Shared/DDD/Entity.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\DDD;
|
||||
|
||||
/**
|
||||
* @template T of EntityId
|
||||
*/
|
||||
interface Entity
|
||||
{
|
||||
/** @return T */
|
||||
public function id(): EntityId;
|
||||
}
|
||||
31
src/Shared/DDD/EntityId.php
Normal file
31
src/Shared/DDD/EntityId.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\DDD;
|
||||
|
||||
use DomainException;
|
||||
use Stringable;
|
||||
|
||||
abstract readonly class EntityId implements Stringable, Equatable
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (trim($value) === '') {
|
||||
throw new DomainException('EntityId value cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof static && $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
10
src/Shared/DDD/Equatable.php
Normal file
10
src/Shared/DDD/Equatable.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\DDD;
|
||||
|
||||
interface Equatable
|
||||
{
|
||||
public function equals(mixed $other): bool;
|
||||
}
|
||||
11
src/Shared/DDD/Repository.php
Normal file
11
src/Shared/DDD/Repository.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\DDD;
|
||||
|
||||
/**
|
||||
*/
|
||||
interface Repository
|
||||
{
|
||||
}
|
||||
15
src/Shared/Exceptions/InvalidUuidException.php
Normal file
15
src/Shared/Exceptions/InvalidUuidException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidUuidException extends InvalidArgumentException
|
||||
{
|
||||
public static function forValue(string $uuid): self
|
||||
{
|
||||
return new self("Invalid UUID: {$uuid}");
|
||||
}
|
||||
}
|
||||
88
src/Shared/Helpers/UuidHelper.php
Normal file
88
src/Shared/Helpers/UuidHelper.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Helpers;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Ramsey\Uuid\Rfc4122\FieldsInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Shared\Exceptions\InvalidUuidException;
|
||||
|
||||
final readonly class UuidHelper
|
||||
{
|
||||
/**
|
||||
* Check if uuid is v7 or not.
|
||||
*/
|
||||
public static function isV7(UuidInterface $uuid): bool
|
||||
{
|
||||
return self::version($uuid) === 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of uuid.
|
||||
*/
|
||||
public static function version(UuidInterface $uuid): int
|
||||
{
|
||||
$fields = $uuid->getFields();
|
||||
|
||||
if ($fields instanceof FieldsInterface) {
|
||||
$version = $fields->getVersion();
|
||||
|
||||
if ($version !== null) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
|
||||
$uuidString = $uuid->toString();
|
||||
|
||||
if (strlen($uuidString) >= 15) {
|
||||
return (int) hexdec($uuidString[14]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate uuid.
|
||||
*/
|
||||
public static function validateUuid(UuidInterface $uuid): void
|
||||
{
|
||||
if (! Uuid::isValid($uuid->toString())) {
|
||||
throw InvalidUuidException::forValue($uuid->toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date time from uuid.
|
||||
*/
|
||||
public static function toDateTime(UuidInterface $uuid): DateTimeImmutable
|
||||
{
|
||||
if (! self::isV7($uuid)) {
|
||||
throw new InvalidUuidException("Cannot extract datetime from non-V7 UUID: {$uuid->toString()}");
|
||||
}
|
||||
|
||||
$fields = $uuid->getFields();
|
||||
|
||||
if ($fields instanceof FieldsInterface) {
|
||||
$timeMsHex = $fields->getTimeLow()->toString() . $fields->getTimeMid()->toString();
|
||||
} else {
|
||||
$hex = str_replace('-', '', $uuid->toString());
|
||||
$timeMsHex = substr($hex, 0, 12);
|
||||
}
|
||||
|
||||
$timeMs = (int) hexdec($timeMsHex);
|
||||
$sec = intdiv($timeMs, 1000);
|
||||
$ms = $timeMs % 1000;
|
||||
|
||||
$dateTime = DateTimeImmutable::createFromFormat('U.u', sprintf('%d.%06d', $sec, $ms * 1000));
|
||||
|
||||
if ($dateTime === false) {
|
||||
throw new InvalidUuidException("Failed to extract datetime from UUID: {$uuid->toString()}");
|
||||
}
|
||||
|
||||
return $dateTime->setTimezone(new DateTimeZone('UTC'));
|
||||
}
|
||||
}
|
||||
60
src/Shared/Models/ValueObjects/Email.php
Normal file
60
src/Shared/Models/ValueObjects/Email.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Models\ValueObjects;
|
||||
|
||||
use DomainException;
|
||||
use Shared\DDD\Equatable;
|
||||
|
||||
/**
|
||||
* Represents a valid email.
|
||||
*/
|
||||
final readonly class Email implements Equatable
|
||||
{
|
||||
private string $value;
|
||||
|
||||
/**
|
||||
* @param string $value A valid email address.
|
||||
* @throws DomainException
|
||||
*/
|
||||
public function __construct(
|
||||
string $value,
|
||||
) {
|
||||
$trimmed = trim($value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new DomainException('Email cannot be empty.');
|
||||
}
|
||||
if (mb_strlen($trimmed) > 2048) {
|
||||
throw new DomainException('Email exceeds the maximum allowed length.');
|
||||
}
|
||||
if (filter_var($trimmed, FILTER_VALIDATE_EMAIL) === false) {
|
||||
throw new DomainException(sprintf('"%s" is not a valid email.', $value));
|
||||
}
|
||||
|
||||
$this->value = mb_strtolower($trimmed, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email address value.
|
||||
*/
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two email addresses for equality.
|
||||
*
|
||||
* @param mixed $other Email to compare with.
|
||||
*/
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
if (! $other instanceof self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
59
src/Shared/Models/ValueObjects/PicturePath.php
Normal file
59
src/Shared/Models/ValueObjects/PicturePath.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Models\ValueObjects;
|
||||
|
||||
use DomainException;
|
||||
use Shared\DDD\Equatable;
|
||||
|
||||
/**
|
||||
* Represents the path to an image file.
|
||||
*/
|
||||
final readonly class PicturePath implements Equatable
|
||||
{
|
||||
public const array ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
|
||||
/**
|
||||
* @param string $value path of picture
|
||||
* @throws DomainException
|
||||
*/
|
||||
public function __construct(
|
||||
private string $value,
|
||||
) {
|
||||
$trimmed = trim($this->value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
throw new DomainException('Picture Path cannot be empty.');
|
||||
}
|
||||
if (mb_strlen($trimmed) > 2048) {
|
||||
throw new DomainException('Picture Path exceeds the maximum allowed length.');
|
||||
}
|
||||
$extension = strtolower(pathinfo($trimmed, PATHINFO_EXTENSION));
|
||||
if (! in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
|
||||
throw new DomainException('Picture Path must end with one of: ' . implode(', ', self::ALLOWED_EXTENSIONS));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path name value.
|
||||
*/
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two path addresses for equality.
|
||||
*
|
||||
* @param mixed $other Picture path to compare with.
|
||||
*/
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
if (! $other instanceof self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
31
tests/Architecture/ApplicationTest.php
Normal file
31
tests/Architecture/ApplicationTest.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
describe('application coding rules', function (): void {
|
||||
arch('no Domain Entities are used in Controllers')
|
||||
->expect('Application\\Http\\Controllers')
|
||||
->not->toUse('Domain\\Models\\Entities');
|
||||
|
||||
arch('no Domain Entities are used in Policies')
|
||||
->expect('Application\\Policies')
|
||||
->not->toUse('Domain\\Models\\Entities');
|
||||
|
||||
arch('app Repositories implement Application\Contracts\Repository')
|
||||
->expect('Application\\Repositories')
|
||||
->toImplement('Application\\Contracts\\Repository');
|
||||
|
||||
arch('app Directories implement Application\Contracts\Directory')
|
||||
->expect('Application\\Directories')
|
||||
->toImplement('Application\\Contracts\\Directory');
|
||||
|
||||
arch('app DTOs are final readonly classes')
|
||||
->expect('Application\\Dto')
|
||||
->classes()
|
||||
->toBeFinal()
|
||||
->toBeReadonly();
|
||||
|
||||
arch('no debug calls in production code')
|
||||
->expect(['dd', 'dump', 'die', 'var_dump', 'sleep'])
|
||||
->not->toBeUsed();
|
||||
});
|
||||
52
tests/Architecture/CodingStandardsTest.php
Normal file
52
tests/Architecture/CodingStandardsTest.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
describe('coding standards', function (): void {
|
||||
arch()->preset()->php();
|
||||
arch()->preset()->security();
|
||||
|
||||
arch('it has no shell execution functions are used')
|
||||
->expect(['exec', 'shell_exec', 'system', 'passthru', 'proc_open'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no eval usage')
|
||||
->expect(['eval'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no dangerous file functions')
|
||||
->expect(['file_get_contents', 'file_put_contents', 'fopen'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no globals are used')
|
||||
->expect(['$GLOBALS', 'global'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no singletons')
|
||||
->expect(['getInstance', 'instance'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no insecure hashing')
|
||||
->expect(['md5', 'sha1'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no sleep function are used')
|
||||
->expect(['sleep', 'usleep'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('it has no debug globals are used')
|
||||
->expect(['var_dump', 'die', 'print_r'])
|
||||
->not->toBeUsed();
|
||||
|
||||
arch('all app source files use strict types')
|
||||
->expect('Application')
|
||||
->toUseStrictTypes();
|
||||
|
||||
arch('all domain source files use strict types')
|
||||
->expect('Domain')
|
||||
->toUseStrictTypes();
|
||||
|
||||
arch('all shared source files use strict types')
|
||||
->expect('Shared')
|
||||
->toUseStrictTypes();
|
||||
});
|
||||
47
tests/Architecture/DomainTest.php
Normal file
47
tests/Architecture/DomainTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shared\DDD\Entity;
|
||||
use Shared\DDD\Repository;
|
||||
|
||||
describe('domain coding rules', function (): void {
|
||||
arch('entities implements Entity')
|
||||
->expect('Domain\\Models\\Entities')
|
||||
->toImplement(Entity::class);
|
||||
|
||||
arch('entities are immutable (disabled due to mock-final-classes conflict)')
|
||||
->expect('Domain\\Models\\Entities')
|
||||
->toBeReadonly()
|
||||
->toBeFinal();
|
||||
|
||||
arch('valueObjects are immutable (disabled due to mock-final-classes conflict)')
|
||||
->expect('Domain\\Models\\ValueObjects')
|
||||
->toBeReadonly()
|
||||
->toBeFinal();
|
||||
|
||||
arch('ports contains only interfaces')
|
||||
->expect('Domain\\Ports')
|
||||
->classes()
|
||||
->toBeInterface();
|
||||
|
||||
arch('port Repositories extend Shared\DDD\Repository')
|
||||
->expect('Domain\\Ports\\Repositories')
|
||||
->toExtend(Repository::class);
|
||||
|
||||
arch('no Application classes are used in Domain')
|
||||
->expect('Domain')
|
||||
->not->toUse('Application');
|
||||
|
||||
arch('no Infrastructure classes are used in Domain')
|
||||
->expect('Domain')
|
||||
->not->toUse('Infrastructure');
|
||||
|
||||
arch('no Illuminate classes are used in Domain')
|
||||
->expect('Domain')
|
||||
->not->toUse('Illuminate');
|
||||
|
||||
arch('no Symfony components are used in Domain')
|
||||
->expect('Domain')
|
||||
->not->toUse('Symfony');
|
||||
});
|
||||
63
tests/Architecture/NamingConventionsTest.php
Normal file
63
tests/Architecture/NamingConventionsTest.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
describe('naming conventions in application', function (): void {
|
||||
arch('casts have Cast suffix')
|
||||
->expect('Application\\Cast')
|
||||
->toHaveSuffix('Cast');
|
||||
|
||||
arch('adapters repositories have Repository suffix')
|
||||
->expect('Application\\Adapters\\Repositories')
|
||||
->toHaveSuffix('Repository');
|
||||
|
||||
arch('exceptions have Exception suffix')
|
||||
->expect('Application\\Exceptions')
|
||||
->toHaveSuffix('Exception');
|
||||
|
||||
arch('controllers have Controller suffix')
|
||||
->expect('Application\\Http\\Controllers')
|
||||
->toHaveSuffix('Controller');
|
||||
|
||||
arch('requests have Request suffix')
|
||||
->expect('Application\\Http\\Requests')
|
||||
->toHaveSuffix('Request');
|
||||
|
||||
arch('resources have Resource suffix')
|
||||
->expect('Application\\Http\\Resources')
|
||||
->toHaveSuffix('Resource');
|
||||
|
||||
arch('policies have Policy suffix')
|
||||
->expect('Application\\Policies')
|
||||
->toHaveSuffix('Policy');
|
||||
|
||||
arch('providers have Provider suffix')
|
||||
->expect('Application\\Providers')
|
||||
->toHaveSuffix('Provider');
|
||||
|
||||
arch('services have Service suffix')
|
||||
->expect('Application\\Services')
|
||||
->toHaveSuffix('Service');
|
||||
});
|
||||
|
||||
describe('naming conventions in domain', function (): void {
|
||||
arch('dtos have Dto suffix')
|
||||
->expect('Domain\\Dto')
|
||||
->toHaveSuffix('Dto');
|
||||
|
||||
arch('exceptions have Exception suffix')
|
||||
->expect('Domain\\Exceptions')
|
||||
->toHaveSuffix('Exception');
|
||||
|
||||
arch('policies have Policy suffix')
|
||||
->expect('Domain\\Policies')
|
||||
->toHaveSuffix('Policy');
|
||||
|
||||
arch('port repositories have Repository suffix')
|
||||
->expect('Domain\\Ports\\Repositories')
|
||||
->toHaveSuffix('Repository');
|
||||
|
||||
arch('domain services have Service suffix')
|
||||
->expect('Domain\\Services')
|
||||
->toHaveSuffix('Service');
|
||||
});
|
||||
47
tests/Architecture/SharedTest.php
Normal file
47
tests/Architecture/SharedTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shared\DDD\Entity;
|
||||
use Shared\DDD\Repository;
|
||||
|
||||
describe('shared coding rules', function (): void {
|
||||
arch('entities implements Entity')
|
||||
->expect('Shared\\Domain\\Models\\Entities')
|
||||
->toImplement(Entity::class);
|
||||
|
||||
arch('entities are immutable (disabled due to mock-final-classes conflict)')
|
||||
->expect('Shared\\Entities')
|
||||
->toBeReadonly()
|
||||
->toBeFinal();
|
||||
|
||||
arch('valueObjects are immutable (disabled due to mock-final-classes conflict)')
|
||||
->expect('Shared\\ValueObjects')
|
||||
->toBeReadonly()
|
||||
->toBeFinal();
|
||||
|
||||
arch('ports contains only interfaces')
|
||||
->expect('Shared\\Ports')
|
||||
->classes()
|
||||
->toBeInterface();
|
||||
|
||||
arch('port Repositories extend Shared\DDD\Repository')
|
||||
->expect('Shared\\Ports\\Repositories')
|
||||
->toExtend(Repository::class);
|
||||
|
||||
arch('no Application classes are used in Shared')
|
||||
->expect('Shared')
|
||||
->not->toUse('Application');
|
||||
|
||||
arch('no Infrastructure classes are used in Shared')
|
||||
->expect('Shared')
|
||||
->not->toUse('Infrastructure');
|
||||
|
||||
arch('no Illuminate classes are used in Shared')
|
||||
->expect('Shared')
|
||||
->not->toUse('Illuminate');
|
||||
|
||||
arch('no Symfony components are used in Shared')
|
||||
->expect('Shared')
|
||||
->not->toUse('Symfony');
|
||||
});
|
||||
40
tests/Pest.php
Normal file
40
tests/Pest.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use DG\BypassFinals;
|
||||
use Tests\TestCase;
|
||||
|
||||
// Prevents mock-final-classes/bypass-finals from removing final/readonly.
|
||||
if (class_exists(BypassFinals::class)) {
|
||||
BypassFinals::denyPaths([
|
||||
dirname(__DIR__) . '/src/*',
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Test Case
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
* case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
* need to change it using the "pest()" function to bind different classes or traits.
|
||||
*
|
||||
*/
|
||||
|
||||
pest()->extend(TestCase::class)
|
||||
->in('Architecture', 'Unit');
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Expectations
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
* "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
* to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
*
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', fn () => $this->toBe(1));
|
||||
10
tests/TestCase.php
Normal file
10
tests/TestCase.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
41
tests/Unit/Domain/Models/Entities/ProductTest.php
Normal file
41
tests/Unit/Domain/Models/Entities/ProductTest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Domain\Models\Entities\ValueObjects;
|
||||
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Models\ValueObjects\ProductId;
|
||||
|
||||
describe('product', function (): void {
|
||||
it('creates a product with a generated id and a name', function (): void {
|
||||
$productName = 'New Product';
|
||||
$productId = ProductId::generate();
|
||||
$product = new Product($productId, $productName);
|
||||
|
||||
expect($product->id())->toBe($productId)
|
||||
->and($product->name())->toBe($productName);
|
||||
});
|
||||
|
||||
it('rejects an empty name', function (): void {
|
||||
expect(fn (): Product => new Product(ProductId::generate(), ''))
|
||||
->toThrow(\DomainException::class, 'Product name cannot be empty.');
|
||||
});
|
||||
|
||||
it('considers two products with the same id as equal', function (): void {
|
||||
$productId = ProductId::generate();
|
||||
|
||||
$first = new Product($productId, 'New Product 1');
|
||||
$second = new Product($productId, 'New Product 2');
|
||||
|
||||
expect($first->equals($second))->toBeTrue();
|
||||
});
|
||||
|
||||
it('considers two products with different ids as not equal', function (): void {
|
||||
$productName = 'New Product';
|
||||
$first = new Product(ProductId::generate(), $productName);
|
||||
$second = new Product(ProductId::generate(), $productName);
|
||||
|
||||
expect($first->equals($second))->toBeFalse();
|
||||
});
|
||||
});
|
||||
78
tests/Unit/Shared/Models/ValueObjects/EmailTest.php
Normal file
78
tests/Unit/Shared/Models/ValueObjects/EmailTest.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\Models\ValueObjects;
|
||||
|
||||
use DomainException;
|
||||
use Shared\Models\ValueObjects\Email;
|
||||
use stdClass;
|
||||
|
||||
covers(Email::class);
|
||||
|
||||
describe('email', function (): void {
|
||||
it('creates a valid email', function (): void {
|
||||
// GIVEN
|
||||
$emailValue = ' Contact@Example.COM ';
|
||||
|
||||
// WHEN
|
||||
$email = new Email($emailValue);
|
||||
|
||||
// THEN
|
||||
expect($email)->toBeInstanceOf(Email::class)
|
||||
->and($email->value())->toBe('contact@example.com');
|
||||
});
|
||||
|
||||
it('rejects an empty email', function (): void {
|
||||
// GIVEN
|
||||
$emailValue = '';
|
||||
|
||||
// WHEN
|
||||
$action = fn (): Email => new Email($emailValue);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(DomainException::class, 'Email cannot be empty.');
|
||||
});
|
||||
|
||||
it('rejects invalid email formats', function (string $invalid): void {
|
||||
// GIVEN & WHEN & THEN
|
||||
expect(fn (): Email => new Email($invalid))->toThrow(DomainException::class);
|
||||
})->with([
|
||||
'',
|
||||
' ',
|
||||
'not-an-email',
|
||||
'@example.com',
|
||||
'contact@',
|
||||
'contact@.com',
|
||||
'contact@com',
|
||||
str_repeat('a', 2048) . '@example.fr'
|
||||
]);
|
||||
|
||||
it('considers two emails with the same value as equal', function (): void {
|
||||
// GIVEN
|
||||
$first = new Email('contact@example.fr');
|
||||
$second = new Email('contact@example.fr');
|
||||
|
||||
// WHEN
|
||||
$result = $first->equals($second);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('considers an email as not equal to', function (mixed $invalid): void {
|
||||
// GIVEN
|
||||
$email = new Email('contact-1@example.fr');
|
||||
|
||||
// WHEN
|
||||
$result = $email->equals($invalid);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeFalse();
|
||||
|
||||
// GIVEN
|
||||
})->with([
|
||||
'different email' => new Email('contact-2@example.fr'),
|
||||
'non-email value' => new stdClass(),
|
||||
]);
|
||||
});
|
||||
98
tests/Unit/Shared/Models/ValueObjects/PicturePathTest.php
Normal file
98
tests/Unit/Shared/Models/ValueObjects/PicturePathTest.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\Models\ValueObjects;
|
||||
|
||||
use DomainException;
|
||||
use Shared\Models\ValueObjects\PicturePath;
|
||||
use stdClass;
|
||||
|
||||
covers(PicturePath::class);
|
||||
|
||||
describe('picture path', function (): void {
|
||||
it('creates a valid PicturePath', function (): void {
|
||||
// GIVEN
|
||||
$pathValue = 'coupons/logo.png';
|
||||
|
||||
// WHEN
|
||||
$path = new PicturePath($pathValue);
|
||||
|
||||
// THEN
|
||||
expect($path)->toBeInstanceOf(PicturePath::class)
|
||||
->and($path->value())->toBe($pathValue);
|
||||
});
|
||||
|
||||
it('rejects an empty path', function (): void {
|
||||
// GIVEN
|
||||
$pathValue = '';
|
||||
|
||||
// WHEN
|
||||
$action = fn (): PicturePath => new PicturePath($pathValue);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(DomainException::class, 'Picture Path cannot be empty.');
|
||||
});
|
||||
|
||||
it('rejects a path that exceeds the maximum allowed length', function (): void {
|
||||
// GIVEN
|
||||
$pathValue = str_repeat('a', 2049) . '.jpg';
|
||||
|
||||
// WHEN
|
||||
$action = fn (): PicturePath => new PicturePath($pathValue);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(DomainException::class, 'Picture Path exceeds the maximum allowed length.');
|
||||
});
|
||||
|
||||
it('accepts a path of exactly the maximum length', function (): void {
|
||||
// GIVEN
|
||||
$pathValue = str_repeat('a', 2044) . '.jpg';
|
||||
|
||||
// WHEN
|
||||
$path = new PicturePath($pathValue);
|
||||
|
||||
// THEN
|
||||
expect($path)->toBeInstanceOf(PicturePath::class)
|
||||
->and($path->value())->toHaveLength(2048);
|
||||
});
|
||||
|
||||
it('rejects a path without an image extension', function (): void {
|
||||
// GIVEN
|
||||
$pathValue = 'image';
|
||||
|
||||
// WHEN
|
||||
$action = fn (): PicturePath => new PicturePath($pathValue);
|
||||
|
||||
// THEN
|
||||
expect($action)->toThrow(DomainException::class, 'Picture Path must end with one of: ' . implode(', ', PicturePath::ALLOWED_EXTENSIONS));
|
||||
});
|
||||
|
||||
it('considers two paths to be equal if they have the same value', function (): void {
|
||||
// GIVEN
|
||||
$first = new PicturePath('coupons/logo.png');
|
||||
$second = new PicturePath('coupons/logo.png');
|
||||
|
||||
// WHEN
|
||||
$result = $first->equals($second);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('considers a logo as not equal to', function (mixed $invalid): void {
|
||||
// GIVEN
|
||||
$logo = new PicturePath('coupons/logo-1.png');
|
||||
|
||||
// WHEN
|
||||
$result = $logo->equals($invalid);
|
||||
|
||||
// THEN
|
||||
expect($result)->toBeFalse();
|
||||
|
||||
// GIVEN
|
||||
})->with([
|
||||
'different picture path' => new PicturePath('coupons/logo-2.png'),
|
||||
'non-picture-path value' => new stdClass(),
|
||||
]);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue