{"id":15068597,"url":"https://github.com/jeckel-lab/contract","last_synced_at":"2025-04-10T17:41:21.480Z","repository":{"id":44642909,"uuid":"272364498","full_name":"Jeckel-Lab/contract","owner":"Jeckel-Lab","description":"A set of contracts and interfaces to use in different packages","archived":false,"fork":false,"pushed_at":"2024-03-25T15:10:25.000Z","size":91,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-26T08:20:30.055Z","etag":null,"topics":["command-pattern","contracts","ddd","ddd-patterns","dto","entity","events","force-immutability","identity","php8","psalm-templating","valueobject"],"latest_commit_sha":null,"homepage":"https://jeckel-lab.fr","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Jeckel-Lab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-15T06:56:18.000Z","updated_at":"2022-08-18T14:34:33.000Z","dependencies_parsed_at":"2024-09-25T01:38:32.578Z","dependency_job_id":"170b32aa-d6e3-4036-83ed-b49936ea9e1e","html_url":"https://github.com/Jeckel-Lab/contract","commit_stats":{"total_commits":75,"total_committers":2,"mean_commits":37.5,"dds":"0.026666666666666616","last_synced_commit":"33ed69e14b89c220bf9452fed46bbac9ea9c6d2a"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jeckel-Lab%2Fcontract","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jeckel-Lab%2Fcontract/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jeckel-Lab%2Fcontract/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jeckel-Lab%2Fcontract/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Jeckel-Lab","download_url":"https://codeload.github.com/Jeckel-Lab/contract/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248262186,"owners_count":21074258,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["command-pattern","contracts","ddd","ddd-patterns","dto","entity","events","force-immutability","identity","php8","psalm-templating","valueobject"],"created_at":"2024-09-25T01:38:27.473Z","updated_at":"2025-04-10T17:41:21.458Z","avatar_url":"https://github.com/Jeckel-Lab.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Latest Stable Version](https://poser.pugx.org/jeckel-lab/contract/v/stable)](https://packagist.org/packages/jeckel-lab/contract)\n[![Total Downloads](https://poser.pugx.org/jeckel-lab/contract/downloads)](https://packagist.org/packages/jeckel-lab/contract)\n[![Build Status](https://travis-ci.org/Jeckel-Lab/contract.svg?branch=master)](https://travis-ci.org/Jeckel-Lab/contract)\n[![codecov](https://codecov.io/gh/Jeckel-Lab/contract/branch/main/graph/badge.svg?token=88XDTqqdar)](https://codecov.io/gh/Jeckel-Lab/contract)\n[![Mutation testing badge](https://img.shields.io/endpoint?style=flat\u0026url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FJeckel-Lab%2Fcontract%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/Jeckel-Lab/contract/main)\n\n# Jeckel-Lab Contract\n\nList of interfaces use as contract in other packages or DD projects\n\nThis contract includes some strong typings, object relation and psalm validation.\n\nRequire **`php \u003e= 7.2.*`** and **`php \u003e= 8.0`**\n\n| Release name | Branch name | Php Version             |\n|--------------|-------------|-------------------------|\n| 1.x          | release/1.X | php \u003e= 7.2 \u0026 php \u003c= 8.0 |\n| 2.x          | master      | php \u003e= 8.0              |\n\n# Documentation for version 2.x (php \u003e= 8.0)\n\n## Domain\n\nDomain contract are part of DDD implementation suggestion, it's not required and is not linked to any frameworks.\n\n### Identity\n\n[Identity](src/Domain/Identity/Identity.php) are used to define a unique identifier for an Entity or a RootAggregate.\n\nIdentity must be:\n- immutable\n- final\n- constructor should be private, use a factory method:\n  - `new` ==\u003e Generate (if possible) a new Identity object with a random value (like UUIDs)\n  - `from` ==\u003e Instantiate Identity from an existing value\n\n\u003e See detailed implementation proposal: [jeckel-lab/identity-contract](https://github.com/Jeckel-Lab/identity-contract)\n\n### Entity\n\n**[Entity](src/Domain/Entity/Entity.php)**: main **Entity** contract\n\nEntity **must** have an Id implementing the `Identity` interface.\n\nDon't forget to use `@psalm templates`\n```php\n/**\n * DiverId is using an `int` as unique identifier\n * @implements Identity\u003cint\u003e\n */\nfinal class DriverId implements Identity\n{\n}\n\n/**\n * Now Driver can use a DriverId as an identifier\n * @implements Entity\u003cDriverId\u003e\n */\nclass Driver implements Entity\n{\n    public function __construct(private DriverId $id)\n    {\n    }\n    \n    /**\n     * @return DriverId\n     */\n    public function getId(): Identity\n    {\n        return $id;\n    }\n}\n```\n\n### Event\n\n[Event](src/Domain/Event/Event.php) are notification about what happened during a use case.\n\nEvent **must** be:\n- immutable\n\n### DomainEventAware\n\n**Entities** and **root aggregates** handle domain events.\nTo facilitate this behaviour, you can use this **interface** and **trait**:\n- **[DomainEventAwareInterface](src/Domain/Entity/DomainEventAwareInterface.php)**\n- **[DomainEventAwareTrait](src/Domain/Entity/DomainEventAwareTrait.php)**\n\nThis interface defines two methods:\n```php\n    /**\n     * @param Event ...$events\n     * @return static\n     */\n    public function addDomainEvent(Event ...$events): static;\n\n    /**\n     * @return list\u003cEvent\u003e\n     */\n    public function popEvents(): array;\n\n```\n- `addDomainEvent` allow you to register new event occurred during a Use Case.\n- `popEvent` will empty the entity's event list at the end of a use case to dispatch them into an Event Dispatcher.\n\nJust use the interface and trait into your entity:\n```Php\nclass MyEntity implement DomainEventAwareInterface\n{\n    use DomainEventAwareTrait;\n    \n    /**\n     * Example of a use case that add an event to the queue\n     * @return self\n     */\n    public function activateEntity(): self\n    {\n        $this-\u003eactivated = true;\n        $this-\u003eaddDomainEvent(new EntityActivated($this-\u003eid));\n        return $this;\n    }\n    \n    //...\n}\n```\n\nAnd if you use the CommandBus pattern, then you can add events to the response easily:\n```php\nnew CommandResponse(events: $entity-\u003epopEvents());\n```\n\n### ValueObject\n\nUsing `ValueObject` to embed a value (or group of value for complex types) as an object allow you:\n- to use strong typing in the application (a `Speed` can not be mixed with any random float)\n- to embed data validation (be sure that the `Speed` is always a positive value, is lower than a reasonable value, etc.)\n\nValue object must be defined as:\n- immutable (one's instantiated, they should not be modified unless a new instance is created).\n- final\n- constructor should be private, use the static `from` method as a factory\n- when requesting to ValueObject with same value, `from` should return the same instance\n\nThink about implementing it like this:\n```Php\nfinal class Speed implements ValueObject, ValueObjectFactory\n{\n    private static $instances = [];\n\n    private function __constructor(private float $speed)\n    {\n    }\n    \n    /**\n     * @param mixed $value\n     * @return static\n     * @throws InvalidArgumentException\n     */\n    public static function from(mixed $speedValue): static\n    {\n        if (! self::$instances[$speedValue]) {\n            if ($speedValue \u003c 0) {\n                throw new InvalidArgumentException('Speed needs to be positive');\n            }\n            self::$instances[$speedValue] = new self($speedValue);\n        }\n        self::$instances[$speedValue]\n    }\n    \n    // implements other methods\n}\n\n// And now\n$speed1 = Speed::from(85.2);\n$speed2 = Speed::from(85.2);\n$speed1 === $speed2; // is true\n```\n\n## Core\n\n\u003e To be completed\n\n### Command Dispatcher\n\n\u003e To be completed\n\n\u003e See detailed implementation proposal: [jeckel-lab/command-dispatcher](https://github.com/Jeckel-Lab/command-dispatcher)\n\n### Query Dispatcher\n\n\u003e To be completed\n\n\u003e See detailed implementation proposal: [jeckel-lab/query-dispatcher](https://github.com/Jeckel-Lab/query-dispatcher)\n\n## Exceptions\n\nEach layer has it's own Exception interface that extends `Throwable`:\n\n- Core: [CoreException](src/Core/Exception/CoreException.php)\n- Domain: [DomainException](src/Domain/Exception/DomainException.php)\n- Infrastructure: [InfrastructureException](src/Infrastructure/Exception/InfrastructureException.php)\n- Presentation: [PresentationException](src/Presentation/Exception/PresentationException.php)\n\nIn each layer, when we need to throw an Exception, we create a new class corresponding to the type of Exception. This class must:\n\n- extends one of the [SPL exception](https://www.php.net/manual/en/spl.exceptions.php) or another (more generic) exception from the same namespace.\n- implements the exception interface of the current layer.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeckel-lab%2Fcontract","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeckel-lab%2Fcontract","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeckel-lab%2Fcontract/lists"}