{"id":19005340,"url":"https://github.com/f3ath/story","last_synced_at":"2026-02-24T07:32:42.480Z","repository":{"id":66349637,"uuid":"120957878","full_name":"f3ath/story","owner":"f3ath","description":null,"archived":false,"fork":false,"pushed_at":"2017-12-14T09:10:22.000Z","size":10,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-29T21:48:00.518Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/f3ath.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2018-02-09T21:31:41.000Z","updated_at":"2018-04-13T23:25:00.000Z","dependencies_parsed_at":"2023-02-21T15:45:58.993Z","dependency_job_id":null,"html_url":"https://github.com/f3ath/story","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/f3ath/story","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/f3ath%2Fstory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/f3ath%2Fstory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/f3ath%2Fstory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/f3ath%2Fstory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/f3ath","download_url":"https://codeload.github.com/f3ath/story/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/f3ath%2Fstory/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29774917,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T04:54:30.205Z","status":"ssl_error","status_checked_at":"2026-02-24T04:53:58.628Z","response_time":75,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2024-11-08T18:27:12.969Z","updated_at":"2026-02-24T07:32:42.456Z","avatar_url":"https://github.com/f3ath.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Designing Domain Logic with PHP in 2018\n\nEarly in 2017 I was helping a friend of mine to come up with a proof of concept for one of\nhis countless ideas. The project was fairly simple - I was implementing an API server for a mobile app.\nThe was already a prototype written in php which used JSON as the data standard. The API was growing fast, so \nto avoid reinventing the wheel, I decided to stick to some sort of a standard. Naturally, I stumbled upon the\n[JSON API](http://jsonapi.org/) standard. By that time a bunch of implementation had been created and available through\n[Composer](https://packagist.org/), but the two I considered the most promising and cleanly written had issues\nwhich prevented me from going ahead and using them. E.g. the way the libraries implemented the idea of serializers broke \nthe letter \"L\" in [SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)). \nAn [issue](https://github.com/tobscure/json-api/issues/115) was open on GitHub,\nand the question of how would I implement that logic arose. Trying to come up with a pull request with a \"right\" solution\nI started plying with the implementation model and matching it with the JSON API standard. Pretty soon I realised\nthere was too many things I wanted to change so I got sucked into the process and when I woke up I realised I had gone\ntoo deep in the woods so that turning this work into a pull request would mean rewriting the entire thing. Yes, a clear\ncase of [14 competing standards](https://xkcd.com/927/), I know. However enough time had been invested, so something complete\nhad to be made out of this. In the following parts I'd like to tell about the process of converting Domain rules\ninto object design with means of modern php and the lessons learned. Despite of being an\n[accidental language](https://motherboard.vice.com/en_us/article/pgkbey/know-your-language-the-wither-of-php), having\na poor type system, and a bunch of bad parts PHP has its own patterns which may be useful and I want to demonstrate \na few of them.\n\n## Setting the expectations\nRewriting the whole library did not make any sense, I focused on the object model of JSON API. To myself, I decided that\nI wanted the create an object model which would serve just one purpose - to implement the rules and restrictions of JSON API\ndefined in the standard. The following framework was set.\n\n### Test-first development\nWas it TDD? Probably not. I tried to follow the [Three Rules](http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd) \nwhen possible. But here is the thing. One of the reasons why TDD is considered useful is that it allows you not to think\nabout code design immediately. The design is supposed to evolve as a sub-product. This idea is sometimes expressed by\nchanging the meaning the last \"D\" - Test-Driven Design instead of Development. In my case the primary source of design\ndecisions was the Domain. Important disclaimer: that was not DDD either. I did not (and neither do) consider myself a\nDDD expert to make such a bold statement. Though, some ideas from the [Big Blue Book](https://domainlanguage.com/ddd/)\nwere possibly (but not necessarily correctly) implemented.\n\n### Each test should reflect one of the standard's requirements\nThe procedure was simple: \n- take one business-rule from the standard\n- write a test case for it\n- make it pass\n- refactor\n- repeat\n\n### Tests should serve as the documentation\nAnother approach I wanted to try out was \n[tests as executable specification](http://agiledata.org/essays/tdd.html#Documentation). Perhaps the real reason was\nme being too lazy to create good docs, but at that moment that's how I explained it to myself.\n\n## The first step\nSo I started with the [Document Structure](http://jsonapi.org/format/#document-structure) and that gave me the simplest\ncase possible: the `Document` class should exist and contain the required media type somewhere:\n```php\n\u003c?php\nclass Document {\n    const MEDIA_TYPE = 'application/vnd.api+json';\n}\n```\n\n## Object instantiation and invalid state\nYou may note that the previous step broke the test-first rule. Yeah, perhaps. But since there was no actual logic to test,\nit's probably not that big of a sin. Anyway, we're going to fix it right now. The next domain rule [said]:(http://jsonapi.org/format/#document-top-level)\n\u003e A document MUST contain at least one of the following top-level members:\n\u003e - data: the document’s “primary data”\n\u003e - errors: an array of error objects\n\u003e - meta: a meta object that contains non-standard meta-information.\n\n\u003e The members data and errors MUST NOT coexist in the same document.\n\nSo it's time to write a Document creation test case. So what could it look like? Clearly, those were `Document`'s \ndependencies, and the a naive implementation would be something like\n```php\n\u003c?php\nclass Document {\n    function __construct($data, $errors, $meta) {}\n}\n```\n\nThis approach had two major drawbacks: any two dependencies can be missing and `data` and `errors` could not coexist.\nViolating these rules would leave the newly created object in an invalid state.\n### Optional dependencies\nWhat do we usually do with an optional dependency in PHP? We can introduce a default value, e.g. `null` or `false`\n(what a horrible choice from the type system's point of view, but I've seen that used a lot).\nIn the OOP world this is called the [Null Object](https://sourcemaking.com/design_patterns/null_object). So let them\nbe optional?\n```php\n\u003c?php\nclass Document {\n    function __construct($data = null, $errors = null, $meta = null) {}\n}\n```\nWell, but now I would have to have some ugly `if`s in the constructor checking that at least one of those has been\nprovided or throw an exception to prevent an invalid state otherwise. It would get even uglier if we added the \ndata/error coexistence check. Luckily, this was easy to solve.\n\n## Named Constructors\n[Method overloading](https://en.wikipedia.org/wiki/Function_overloading) in PHP is far from perfect. But in case of\nconstructors there is a nice workaround called Named Constructors:\n- make `__construct()` private\n- create public static methods with meaningful names and call `__construct()` from there\n\n```php\n\u003c?php\nclass Document {\n    private $data, $meta, $errors;\n    private function __construct() {}\n    static function fromData($data): Document {\n        $doc = new self;\n        $doc-\u003edata = $data;\n        return $doc;\n    }\n    static function fromErrors($data): Document { /* similar logic */ }\n    static function fromMeta($data): Document { /* similar logic */ }\n    function setMeta($meta) {\n        $this-\u003emeta = $meta;\n    }\n}\n```\n\nThis solved the issue. A Document would inevitably have at least one of the dependencies. There was no way to create a \nDocument containing both data and errors. Meta could be combined with data and with errors (via `setMeta()`) or could \nbe used standalone (via `fromMeta()`). Another advantage was that we did not have to remember the order of the arguments\nin the constructor.\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ff3ath%2Fstory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ff3ath%2Fstory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ff3ath%2Fstory/lists"}