{"id":19765084,"url":"https://github.com/azhovan/expose-api-efficient-way","last_synced_at":"2025-06-25T09:04:08.836Z","repository":{"id":110797923,"uuid":"150242783","full_name":"Azhovan/expose-api-efficient-way","owner":"Azhovan","description":"Expose Apis with CQRS Driven approach","archived":false,"fork":false,"pushed_at":"2018-10-31T15:08:59.000Z","size":42,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-06-25T09:03:59.432Z","etag":null,"topics":["builder-design-pattern","event-driven","factory-method-pattern","php72","restful-api"],"latest_commit_sha":null,"homepage":"https://www.slideshare.net/elazhiA/design-expose-ap-is-with-cqrs-116448146","language":"PHP","has_issues":true,"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/Azhovan.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-09-25T09:48:35.000Z","updated_at":"2022-07-27T03:03:52.000Z","dependencies_parsed_at":null,"dependency_job_id":"861543f5-6093-4351-94e1-dd68de583f61","html_url":"https://github.com/Azhovan/expose-api-efficient-way","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Azhovan/expose-api-efficient-way","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azhovan%2Fexpose-api-efficient-way","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azhovan%2Fexpose-api-efficient-way/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azhovan%2Fexpose-api-efficient-way/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azhovan%2Fexpose-api-efficient-way/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Azhovan","download_url":"https://codeload.github.com/Azhovan/expose-api-efficient-way/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azhovan%2Fexpose-api-efficient-way/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261841935,"owners_count":23217911,"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":["builder-design-pattern","event-driven","factory-method-pattern","php72","restful-api"],"created_at":"2024-11-12T04:16:35.640Z","updated_at":"2025-06-25T09:04:08.824Z","avatar_url":"https://github.com/Azhovan.png","language":"PHP","readme":"# Table of contents\n-----\n\n1. [Folder structure](#folder-structure)\n2. [Application life cycle](#application-life-cycle)\n3. [Application Architecture and Design implementation](#application-architecture-and-design-implementation)\n   * [Architecture](#architecture)\n   * [Design](#design)\n   * [Architecture Abstractions](#architecture-abstractions)\n   * [Request Abstraction](#request-abstraction)\n   * [Events](#events) \n   * [Dispatch events](#dispatch-events) \n   * [Events](#events) \n4. [Run Tests](#run-tests)\n5. [Endpoints](#endpoints)\n   * [Api Authentication](#api-authentication)\n   * [Api Description](#api-description)\n6. [Persistence](#persistence)\n7. [Code Standards](#code-standards)\n8. [Used Libraries](#used-libraries)\n9. [Requirements](#requirements)\n10. [How To Deploy](#how-to-deploy)\n   \n\n-----\n\n## folder structure \n-----\n\n~~~\n├── docs                  // The documentation files\n│  \n├── helpers               // helper functions\n│    \n├── web                   // entry point of the project\n│   ├── index.php\n│\n├── src                   // The source codes folder\n│   ├── Container         // IoC to Inject/Register services\n|   ├── Controllers       // implementation of controller\n|       └── Request        \n|          └──  Recipe     // Form Request validation for every request  \n|      ├── Response \n|   ├── Core\n|       └── Contracts       // interfaces\n│   ├── Recipe            // Recipe Implementation \n│     └── Core            // Core functionality of Recipe\n|         └── Event\n|         └── Traits\n│     └── Exception \n│   ├── Routes\n├── tests\n~~~\n\n## Application-life-cycle\n-----   \n\nThis project does not use any framework, but it acts like a very simple framework to manage client requests more easily.\nbelow are steps that show how a client request will proceed.\n\n1) client hits an endpoint\n2) application will be bootstrapped by loading dependencies, helper functions and finally registering services into the container\n3) user request captured by the router, an instance of request object and application services injected into the controller\n4) application request will be expanded by an abstraction class to apply filtering, authorizations and etc.\n5) request data will be captured in step 4 and if everything went good, the request can go next step or just terminated and proper message with \nwell prepared HTTP code returned to the user.\n6) specific service(in this case Recipe) will be invoked, data passed into it.\n7) based on requested action an event dispatched to calculate and aggregate the data. \n8) the result will be returned.\n9) the user can see the valid JSON in response.\n\nin all steps, if any exception/error happened it will be propagated into upper layers. \n\n## Application Architecture and Design implementation\n\n### Architecture\n- This project follows **event-driven** architecture. All actions will cause an event in the application\nto control the fellow.\n\n\n### Design\nThe Recipes are broken down into 3 parts :\n1) Recipe Template\n2) Recipe Builder\n3) Recipe events\nwhen a request comes to the application a **Recipe Template** will be created. That template will be filled by data that prepared by the user or by internal behavior. **Recipe Builder** will dynamically trigger a **Event** \nfrom that **context**.All the above parts will be covered below.\n\nLets see quick usage \n```php\nRecipe::create(\n                $data, function (RecipeTemplate $item) use ($id) {\n                    $item-\u003eid($id);\n                    $item-\u003ename();\n                    $item-\u003eprepTime();\n                    $item-\u003edifficulty();\n                    $item-\u003evegetarian();\n                }\n            );\n```\n \n### Architecture Abstractions\n\n-  For expanding the functionality, abstractions will use traits, for example, `RedisPersistence`'s functionality will be expanded by\n`RedisPersistenceTrait`.\n### Request Abstraction\n- Every request can be validated and filters the inputs dynamically. below codes show this abstraction\n\n```php\nabstract class AbstractRequest implements ValidateRequest\n{\n\n    use ValidateRequestTrait, SimplifyRequestBagTrait;\n    /**\n     * instance of request object\n     *\n     * @var Request\n     */\n    protected $requestInstance;\n\n    /**\n     * hold all errors\n     *\n     * @var array of errors\n     */\n    protected $errorBag = [];\n\n    /**\n     * return the object of Request Instance class\n     *\n     * @return mixed\n     */\n    public function getRequestInstance()\n    {\n        return $this-\u003erequestInstance;\n    }\n\n\n    /**\n     * Get the validation rules that apply to the request.\n     *\n     * @return array\n     */\n    abstract protected function rules();\n\n\n    /**\n     * Determine if the user is authorized to make this request.\n     *\n     * @return bool\n     */\n    abstract protected function authorize();\n\n\n    /**\n     * Get the error messages for the defined validation rules.\n     *\n     * @return array\n     */\n    abstract public function messages();\n\n}\n\n```\nlet's see one example of the implementation of the Abstraction in  (`CreateRequest.php`):\n```php\nclass CreateRequest extends AbstractRequest\n{\n\n    public function __construct(Request $request)\n    {\n        $this-\u003erequestInstance = $request;\n    }\n\n\n    /**\n     * Get the validation rules\n     * these rules will be applied to request\n     *\n     * @return array\n     */\n    protected function rules()\n    {\n        return [\n            \"name\" =\u003e [\"required\"],\n            \"prepTime\" =\u003e [\"required\"],\n            \"difficulty\" =\u003e [\"required\"],\n        ];\n    }\n\n    /**\n     * Determine if the user is authorized or not\n     * if false returned , user is not able to access to resource\n     *\n     * @return bool\n     */\n    protected function authorize()\n    {\n        $headers = $this-\u003egetRequestInstance()-\u003eheaders();\n\n        return getAuth($headers);\n    }\n\n    /**\n     * Get the error messages for\n     *   the defined validation rules.\n     *\n     * @return array\n     */\n    public function messages()\n    {\n        return [\n            \"name.required\" =\u003e \"Recipe's name field is required\",\n            \"prepTime.required\" =\u003e \"Recipe's prepTime field is mandatory\",\n            \"difficulty.required\" =\u003e \"Recipe's difficulty field is mandatory\",\n        ];\n    }\n\n\n}\n\n```\nAs you can see the method `rules()` will define the constraints on the request. method `authorize()` will indicate does this request needs **Authorization** or not, and finally `messages()` will show a related error message when any rule failed.\n\n### Events\nsince every request will be converted into an event, the event abstraction will be focused in this project\nbelow is a high-level abstraction for an event interface\n```php\ninterface EventInterface\n{\n    /**\n     * event type\n     *\n     * @return string\n     */\n    public static function getType() : string ;\n\n    /**\n     * get full qualified namespace prefix\n     *\n     * @return string\n     */\n    public static function getContext(): string;\n\n    /**\n     * get the full qualified namespace based on input\n     *\n     * @param  string $event\n     * @return string\n     */\n    public static function getContextFromType(string $event): string;\n\n\n    /**\n     * event handler\n     *\n     * @return string\n     */\n    public function handle();\n\n}\n```\n\nSince this project is developed as a production-ready application, thinking about how to scale it, is important. for satisfy this needs I added one simple Abstraction layer, under the `EventInterface`.\n```php\n\nabstract class AbstractRecipeEvent implements EventInterface\n{\n\n    /**\n     * @var IteratorAggregate\n     */\n    protected $data;\n\n    protected $persistenceDriver;\n\n\n    /**\n     * RecipeCreated constructor.\n     *\n     * @param IteratorAggregate $data\n     */\n    public function __construct(IteratorAggregate $data)\n    {\n        $this-\u003edata = $data;\n        $this-\u003epersistenceDriver = static::getPersistentDriver();\n    }\n\n    /**\n     * @inheritdoc\n     *\n     * @return string\n     */\n    public static function getType(): string\n    {\n        return \"Recipe\";\n    }\n\n    /**\n     * @inheritdoc\n     *\n     * @return string\n     */\n    public static function getContext(): string\n    {\n        return \"\\\\App\\\\ExposeApi\\\\Recipe\\\\Core\\\\Event\\\\\";\n    }\n\n    /**\n     * @inheritdoc\n     *\n     * @param  $event\n     * @return string\n     */\n    public static function getContextFromType(string $event): string\n    {\n        return \"\\\\App\\\\ExposeApi\\\\Recipe\\\\Core\\\\Event\\\\{$event}\";\n    }\n\n    /**\n     * @inheritdoc\n     *\n     * @return string\n     */\n    public abstract function handle();\n\n\n}\n```\n\nAs you can see, the implementation of details will be remains to concrete classes(not in class abstraction).\nlet see one of these implementations in this project.\n\n```php\n\nfinal class RecipeCreated extends AbstractRecipeEvent implements IteratorAggregate, Jsonable\n{\n\n    use RedisTrait;\n\n    /**\n     * event handler\n     * data will be PERSIST in redis\n     *\n     * @return string\n     * @throws \\Exception\n     */\n    public function handle()\n    {\n        $this-\u003esave($this-\u003edata-\u003eid, $this-\u003etoJson());\n\n        return $this-\u003egetOrFail($this-\u003edata-\u003eid);\n    }\n\n\n    /**\n     * @inheritdoc\n     * @return     Traversable|void\n     */\n    public function getIterator()\n    {\n        return $this-\u003edata-\u003egetIterator();\n    }\n\n    /**\n     * @inheritdoc\n     *\n     * @param  int $options\n     * @return string\n     */\n    public function toJson($options = 0)\n    {\n        return $this-\u003edata-\u003egetFluent()-\u003etoJson($options);\n    }\n\n}\n``` \n\n### Dispatch events\nAs mentioned above, the builder pattern used for this project and still is decoupled from event implementations.\n Before that let see how a Recipe class looks like:\n\n```php\n/**\n * Class Recipe\n *\n * @package App\\ExposeApi\\Recipe\n *\n * @method static \\App\\ExposeApi\\Recipe\\Builder create (array $data, \\Closure $callback)\n * @method static \\App\\ExposeApi\\Recipe\\Builder delete (array $id, \\Closure $callback = null)\n * @method static \\App\\ExposeApi\\Recipe\\Builder update (array $data, \\Closure $callback)\n * @method static \\App\\ExposeApi\\Recipe\\Builder get (array $id)\n * @method static \\App\\ExposeApi\\Recipe\\Builder rate (array $data, \\Closure $callback)\n * @method static \\App\\ExposeApi\\Recipe\\Builder search (array $data, \\Closure $callback)\n *\n * @see \\App\\ExposeApi\\Recipe\\Builder\n */\nclass Recipe extends AbstractRecipe\n{\n\n    /**\n     * @inheritdoc\n     *\n     * @return Builder|mixed\n     */\n    public static function getRecipeAccessor()\n    {\n        return new Builder();\n    }\n}\n```\nRecipe class will decide which object is responsible for access to Recipe functionalities .\nAnd the AbstractRecipe class :\n```php\n\u003c?php\n\nnamespace App\\ExposeApi\\Recipe;\n\n\nabstract class AbstractRecipe\n{\n    /**\n     * Get the recipe builder class instance\n     *\n     * @return mixed\n     *\n     * @see \\App\\ExposeApi\\Recipe\\Builder\n     */\n    abstract public static function getRecipeAccessor();\n\n    /**\n     * Handle dynamic, static calls to the object.\n     *\n     * @param  $method\n     * @param  $arguments\n     * @return mixed\n     *\n     * @throws \\RuntimeException\n     */\n    public static function __callStatic($method, $arguments)\n    {\n        $instance = static::getRecipeAccessor();\n\n        if (!$instance) {\n            throw new \\RuntimeException(\"Recipe builder class does not exist\");\n        }\n\n\n        return $instance-\u003e$method(...$arguments);\n    }\n}\n```   \n\n\n## Endpoints\n\n| Name   | Method      | URL                    | Protected |\n| ---    | ---         | ---                    | ---       |\n| List   | `GET`       | `/recipes`             | ✘         |\n| Create | `POST`      | `/recipes`             | ✓         |\n| Get    | `GET`       | `/recipes/{id}`        | ✘         |\n| Update | `PUT/PATCH` | `/recipes/{id}`        | ✓         |\n| Delete | `DELETE`    | `/recipes/{id}`        | ✓         |\n| Rate   | `POST`      | `/recipes/{id}/rating` | ✘         |\n| Search | `POST`      | `/recipes/search`      | ✘         |\n\n### API Authentication\nBelow APIs needs Authorization in the header\n- create\n- update\n- delete\n\nSimply add an `Authorization` header, (Example:` Authorization: AccessKey {accessKey}`). to keep it as simple as in this project\n`{accessKey}` can be any value (it **MUST** not be empty). \n\nfor examples :\n- ` Authorization: AccessKey 123456`  WORKS ✓\n- ` Authorization: AccessKey 98745`   WORKS ✓\n- ` Authorization: AccessKey fdgfgdfgfgf` WORKS ✓\n- ` Authorization: AccessKey` NOT WORKS ✘\n- ` Authorization: ` NOT WORKS ✘\n\n**NOTE**\n- `AccessKey` in ` Authorization: AccessKey 123456` is constant, and is mandatory.\n\n\n\n### API Description\n-----\n\nAPIs that needs **create** or **update** , **search** and **rating**, data **MUST** be passed in body as a valid json.\nfor example: \n~~~\n{\n\t\"name\": \"test name\",\n\t\"prepTime\": \"21 min\",\n\t\"vegetarian\": false,\n\t\"difficulty\": \"Hard\"\n}\n~~~\nAll elements in **search** api will be **AND** together. for example below request means we are searching for a recipe that \nname=jack **AND** difficulty=hard\n~~~\n{\n\t\"name\": \"jack\",\n\t\"difficulty\": \"hard\"\n}\n~~~\n**Rate** Api has below format :\nfor example if you want to rate the recipe with id :`f1d9ae6f-2bb2-42f4-a842-9e9cc658cad2` \n`POST /recipes/f1d9ae6f-2bb2-42f4-a842-9e9cc658cad2/rating\n`\nBody will be : \n~~~\n{\n  \"rate\":5\n}\n~~~\n\n\n## Storage\ndata will be stored as (key, value) in `Redis`. at every update(create/delete/update/rating), data will be persisted in the disk in ASYNC mode. this also triggered as an event\n```php\n/**\n     *  Asynchronously save the dataset to disk (in background)\n     *\n     * @return mixed\n     */\n    public function saveAsync()\n    {\n       return dispatch(RedisPersistence::getContextFromType('RedisPersistence'), $this-\u003epersistenceDriver);\n    }\n\n    /**\n     * save and persist data on disk Asynchronously\n     *\n     * @param $key\n     * @param $value\n     */\n    public function save($key, $value): void\n    {\n        $this-\u003epersistenceDriver-\u003eset($key, $value);\n\n        $this-\u003esaveAsync();\n    }\n```\n\n## Code Standards\nI used `\"squizlabs/php_codesniffer\": \"3.*\"` as `require-dev`and Apply it to codes to make sure PSRs will be in place.\n\n## Used Libraries\n- klein/klein (as php router and service registeration, it is very light weight)\n- ramsey/uui (to generate recipe id)\n- predis/predis (Redis library management)\n- squizlabs/php_codesniffer (PSRs standardize)\n- phpunit/phpunit (unit test framweork)\n\n## Requirements\n- PHP 7.2+\n- PHPUnit 7.0+\n\n## How To Deploy\n\n1) this project will use port `80` to connect to php container, make sure no one is using this port. you can make sure about that \nby running `sudo netstat -nlp | grep 80` command.\n\n2) Run below commands from the project's root: (**all commands need root permission**)\n```\ndocker-compose build\ndocker-compose up -d\ndocker exec -it exposeapi_php bash -c \"composer install\"\n```\n\n## Run Tests\nAll test located at the root of the project. currently `57 tests, 72 assertions` are provided.\n\n**How to run :**\n```\ndocker exec -it exposeapi_php bash -c \"vendor/bin/phpunit tests/\"\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazhovan%2Fexpose-api-efficient-way","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fazhovan%2Fexpose-api-efficient-way","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazhovan%2Fexpose-api-efficient-way/lists"}