feat: entité Website liée à Customer avec UUID, type et state machine

Entity Website :
- customer (ManyToOne, CASCADE) : client propriétaire
- name : nom du site
- uuid : UUID v4 auto-généré (unique, 36 chars)
- type : vitrine | ecommerce
- state : created → install_progress → open → suspended → closed
- createdAt / updatedAt (auto sur setState)
- isOpen() : vérifie si state === open

WebsiteTest (5 tests, 20 assertions) :
- testConstructor : valeurs par défaut, uuid 36 chars, type vitrine
- testConstructorEcommerce : type ecommerce
- testSetters : name, type, updatedAt
- testState : transitions created→install_progress→open→suspended→closed
- testUuidUnique : 2 sites ont des uuid différents

Dépendance : symfony/uid ajouté pour Uuid::v4()
Migration : CREATE TABLE website avec FK customer, uuid unique

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 21:06:41 +02:00
parent 7648946c2b
commit 98db87eb05
5 changed files with 414 additions and 1 deletions

View File

@@ -56,6 +56,7 @@
"symfony/string": "8.0.*", "symfony/string": "8.0.*",
"symfony/translation": "8.0.*", "symfony/translation": "8.0.*",
"symfony/twig-bundle": "8.0.*", "symfony/twig-bundle": "8.0.*",
"symfony/uid": "8.0.*",
"symfony/validator": "8.0.*", "symfony/validator": "8.0.*",
"symfony/web-link": "8.0.*", "symfony/web-link": "8.0.*",
"symfony/yaml": "8.0.*", "symfony/yaml": "8.0.*",

163
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c89df7b95449dfc734150e681bc309f2", "content-hash": "9234633550a505d8b9b7ed8ee0118699",
"packages": [ "packages": [
{ {
"name": "async-aws/core", "name": "async-aws/core",
@@ -8628,6 +8628,89 @@
], ],
"time": "2025-06-23T16:12:55+00:00" "time": "2025-06-23T16:12:55+00:00"
}, },
{
"name": "symfony/polyfill-uuid",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-uuid": "*"
},
"suggest": {
"ext-uuid": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Uuid\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Pineau",
"email": "lyrixx@lyrixx.info"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for uuid functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"uuid"
],
"support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v8.0.5", "version": "v8.0.5",
@@ -10301,6 +10384,84 @@
], ],
"time": "2026-03-04T13:55:34+00:00" "time": "2026-03-04T13:55:34+00:00"
}, },
{
"name": "symfony/uid",
"version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
"reference": "f63fa6096a24147283bce4d29327d285326438e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/uid/zipball/f63fa6096a24147283bce4d29327d285326438e0",
"reference": "f63fa6096a24147283bce4d29327d285326438e0",
"shasum": ""
},
"require": {
"php": ">=8.4",
"symfony/polyfill-uuid": "^1.15"
},
"require-dev": {
"symfony/console": "^7.4|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Uid\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Pineau",
"email": "lyrixx@lyrixx.info"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to generate and represent UIDs",
"homepage": "https://symfony.com",
"keywords": [
"UID",
"ulid",
"uuid"
],
"support": {
"source": "https://github.com/symfony/uid/tree/v8.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
},
{ {
"name": "symfony/validator", "name": "symfony/validator",
"version": "v8.0.7", "version": "v8.0.7",

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260404190626 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE website (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, uuid VARCHAR(36) NOT NULL, type VARCHAR(20) NOT NULL, state VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, customer_id INT NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_476F5DE7D17F50A6 ON website (uuid)');
$this->addSql('CREATE INDEX IDX_476F5DE79395C3F3 ON website (customer_id)');
$this->addSql('ALTER TABLE website ADD CONSTRAINT FK_476F5DE79395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) ON DELETE CASCADE NOT DEFERRABLE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE website DROP CONSTRAINT FK_476F5DE79395C3F3');
$this->addSql('DROP TABLE website');
}
}

129
src/Entity/Website.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity]
class Website
{
public const TYPE_VITRINE = 'vitrine';
public const TYPE_ECOMMERCE = 'ecommerce';
public const STATE_CREATED = 'created';
public const STATE_INSTALL_PROGRESS = 'install_progress';
public const STATE_OPEN = 'open';
public const STATE_SUSPENDED = 'suspended';
public const STATE_CLOSED = 'closed';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Customer::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Customer $customer;
#[ORM\Column(length: 255)]
private string $name;
#[ORM\Column(length: 36, unique: true)]
private string $uuid;
#[ORM\Column(length: 20)]
private string $type;
#[ORM\Column(length: 20)]
private string $state = self::STATE_CREATED;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
public function __construct(Customer $customer, string $name, string $type = self::TYPE_VITRINE)
{
$this->customer = $customer;
$this->name = $name;
$this->type = $type;
$this->uuid = Uuid::v4()->toRfc4122();
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getCustomer(): Customer
{
return $this->customer;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getUuid(): string
{
return $this->uuid;
}
public function getType(): string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getState(): string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
$this->updatedAt = new \DateTimeImmutable();
return $this;
}
public function isOpen(): bool
{
return self::STATE_OPEN === $this->state;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Customer;
use App\Entity\User;
use App\Entity\Website;
use PHPUnit\Framework\TestCase;
class WebsiteTest extends TestCase
{
private function createCustomer(): Customer
{
$user = new User();
$user->setEmail('c@t.com');
$user->setFirstName('C');
$user->setLastName('T');
$user->setPassword('h');
return new Customer($user);
}
public function testConstructor(): void
{
$customer = $this->createCustomer();
$site = new Website($customer, 'Mon Site');
$this->assertNull($site->getId());
$this->assertSame($customer, $site->getCustomer());
$this->assertSame('Mon Site', $site->getName());
$this->assertSame(36, \strlen($site->getUuid()));
$this->assertSame(Website::TYPE_VITRINE, $site->getType());
$this->assertSame(Website::STATE_CREATED, $site->getState());
$this->assertInstanceOf(\DateTimeImmutable::class, $site->getCreatedAt());
$this->assertNull($site->getUpdatedAt());
}
public function testConstructorEcommerce(): void
{
$site = new Website($this->createCustomer(), 'Boutique', Website::TYPE_ECOMMERCE);
$this->assertSame(Website::TYPE_ECOMMERCE, $site->getType());
}
public function testSetters(): void
{
$site = new Website($this->createCustomer(), 'Test');
$site->setName('Nouveau nom');
$this->assertSame('Nouveau nom', $site->getName());
$site->setType(Website::TYPE_ECOMMERCE);
$this->assertSame(Website::TYPE_ECOMMERCE, $site->getType());
$now = new \DateTimeImmutable();
$site->setUpdatedAt($now);
$this->assertSame($now, $site->getUpdatedAt());
}
public function testState(): void
{
$site = new Website($this->createCustomer(), 'Test');
$this->assertFalse($site->isOpen());
$site->setState(Website::STATE_INSTALL_PROGRESS);
$this->assertSame(Website::STATE_INSTALL_PROGRESS, $site->getState());
$this->assertFalse($site->isOpen());
$site->setState(Website::STATE_OPEN);
$this->assertTrue($site->isOpen());
$this->assertInstanceOf(\DateTimeImmutable::class, $site->getUpdatedAt());
$site->setState(Website::STATE_SUSPENDED);
$this->assertFalse($site->isOpen());
$site->setState(Website::STATE_CLOSED);
$this->assertFalse($site->isOpen());
}
public function testUuidUnique(): void
{
$customer = $this->createCustomer();
$site1 = new Website($customer, 'Site 1');
$site2 = new Website($customer, 'Site 2');
$this->assertNotSame($site1->getUuid(), $site2->getUuid());
}
}