{"id":16423571,"url":"https://github.com/rougin/dexterity","last_synced_at":"2025-04-14T09:13:39.362Z","repository":{"id":78364692,"uuid":"126854543","full_name":"rougin/dexterity","owner":"rougin","description":"\"Ready-to-eat\" CRUD code in PHP.","archived":false,"fork":false,"pushed_at":"2024-12-11T02:26:35.000Z","size":115,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-14T09:13:28.862Z","etag":null,"topics":["php-crud","php-depot","php-repository"],"latest_commit_sha":null,"homepage":"https://roug.in/dexterity","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/rougin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","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-03-26T16:04:07.000Z","updated_at":"2024-12-11T02:26:39.000Z","dependencies_parsed_at":"2024-12-10T08:35:45.811Z","dependency_job_id":null,"html_url":"https://github.com/rougin/dexterity","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rougin%2Fdexterity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rougin%2Fdexterity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rougin%2Fdexterity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rougin%2Fdexterity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rougin","download_url":"https://codeload.github.com/rougin/dexterity/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248852182,"owners_count":21171842,"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":["php-crud","php-depot","php-repository"],"created_at":"2024-10-11T07:40:18.994Z","updated_at":"2025-04-14T09:13:39.334Z","avatar_url":"https://github.com/rougin.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dexterity\n\n[![Latest Version on Packagist][ico-version]][link-packagist]\n[![Software License][ico-license]][link-license]\n[![Build Status][ico-build]][link-build]\n[![Coverage Status][ico-coverage]][link-coverage]\n[![Total Downloads][ico-downloads]][link-downloads]\n\n`Dexterity` is a utility PHP package that provides extensible PHP classes for handling [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). It can also create HTTP routes that conforms to the [PSR-07](https://www.php-fig.org/psr/psr-7/) standard.\n\n## Installation\n\nInstall the `Dexterity` package via [Composer](https://getcomposer.org/):\n\n``` bash\n$ composer require rougin/dexterity\n```\n\n## Using `Depot`\n\nThe `Depot` class is a special PHP class which provides methods related to CRUD operations (e.g., `create`, `delete`, `find`, `update`):\n\n``` php\nnamespace Acme\\Depots;\n\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n}\n```\n\nUsing the `Depot` class improves development productivity as it reduces writing of code relating to CRUD operations. As it is also designed to be extensible, it can be used freely without the required methods.\n\n\u003e [!NOTE]\n\u003e In other PHP frameworks and other guides, `Depot` is also known as `Repository` from the [Repository pattern](https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html).\n\nIf a `Depot` class is used, the following methods should be defined depending on its usage:\n\n### `create` method\n\nThe `create` method will be used for creating an item based on the provided payload:\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\n\n$depot = new UserDepot;\n\n/** @var array\u003cstring, mixed\u003e */\n$data = /** ... */;\n\n/** @var \\Acme\\Sample\\User */\n$item = $depot-\u003ecreate($data);\n```\n\nIf the specified method is being called, its logic must be defined from the `Depot` class:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\UserFactory;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    /**\n     * Creates a new item.\n     *\n     * @param array\u003cstring, mixed\u003e $data\n     *\n     * @return \\Acme\\Sample\\User\n     */\n    public function create($data)\n    {\n        return UserFactory::create($data);\n    }\n\n    // ...\n}\n```\n\nIf the required logic for the `create` method is not defined, it will throw a `LogicError`.\n\n### `delete` method\n\nWhen deleting specified items, the `delete` method can be used from the `Depot` class:\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\n\n$depot = new UserDepot;\n\n$depot-\u003edelete(99);\n```\n\nUsing the `delete` method also requires other methods `deleteRow` and `rowExists` to be defined:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\UserDeleter;\nuse Acme\\Sample\\UserReader;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n\n    /**\n     * Checks if the specified item exists.\n     *\n     * @param integer $id\n     *\n     * @return boolean\n     */\n    public function rowExists($id)\n    {\n        return UserReader::exists($id);\n    }\n\n    /**\n     * Deletes the specified item.\n     *\n     * @param integer $id\n     *\n     * @return boolean\n     */\n    protected function deleteRow($id)\n    {\n        return UserDeleter::delete($id);\n    }\n}\n```\n\nIf the required logic for the `delete` method is not defined, a `LogicError` will be thrown.\n\n### `find` method\n\nThe `find` method is one of the CRUD operations that tries to find an item based on the given unique identifier (e.g., `id`):\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\n\n$depot = new UserDepot;\n\n/** @var \\Acme\\Sample\\User */\n$item = $depot-\u003efind(99);\n```\n\nTo use the `find` method, kindly write its logic in the `findRow` method:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\UserReader;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n\n    /**\n     * Returns the specified item.\n     *\n     * @param integer $id\n     *\n     * @return \\Acme\\Sample\\User\n     * @throws \\UnexpectedValueException\n     */\n    protected function findRow($id)\n    {\n        $item = UserReader::find($id);\n\n        if (! $item)\n        {\n            throw new \\UnexpectedValueException('Item not found');\n        }\n\n        return $item;\n    }\n}\n```\n\nIf the specified identifier does not exists, it should throw an `UnexpectedValueException`. Likewise, if the required logic for the `find` method is not defined, it will throw a `LogicError`.\n\n### `get` method\n\nOne of the methods of `Depot` that returns an array of items based on the specified page number and its rows to be shown per page:\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\n\n$depot = new UserDepot;\n\n/** @var \\Rougin\\Dexterity\\Result */\n$item = $depot-\u003eget(1, 10);\n```\n\nTo use the `get` method, the methods `getItems` and `getTotal` should be defined:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\UserReader;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n\n    /**\n     * Returns the total number of items.\n     *\n     * @return integer\n     */\n    public function getTotal()\n    {\n        return UserReader::totalRows();\n    }\n\n    /**\n     * Returns the items with filters.\n     *\n     * @param integer $page\n     * @param integer $limit\n     *\n     * @return \\Acme\\Sample\\User[]\n     */\n    protected function getItems($page, $limit)\n    {\n        return UserReader::getByLimit($limit, $page);\n    }\n}\n```\n\nIf the logic requires an offset instead of a page number, the `getOffset` method from `Depot` can be used to compute the said offset value:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\UserReader;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n\n    /**\n     * Returns the items with filters.\n     *\n     * @param integer $page\n     * @param integer $limit\n     *\n     * @return \\Acme\\Sample\\User[]\n     */\n    protected function getItems($page, $limit)\n    {\n        $offset = $this-\u003egetOffset($page, $limit);\n\n        return UserReader::items($offset, $limit);\n    }\n\n    // ...\n}\n```\n\nUsing the `get` method returns a `Result` class, which can be used for handling the result from the `Depot`:\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\n\n$depot = new UserDepot;\n\n/** @var \\Rougin\\Dexterity\\Result */\n$item = $depot-\u003eget(1, 10);\n\nprint_r($item-\u003etoArray());\n```\n\nEach item from the `Result` class can also be parsed manually using the `parseRow` class:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\User;\nuse Acme\\Sample\\UserReader;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n\n    /**\n     * Returns the parsed item.\n     *\n     * @param \\Acme\\Sample\\User $row\n     *\n     * @return array\u003cstring, mixed\u003e\n     */\n    protected function parseRow(User $row)\n    {\n        $data = array('id' =\u003e $row-\u003eid);\n\n        $data['name'] = $row-\u003ename;\n\n        $data['age'] = $row-\u003eage + 10;\n\n        return $data;\n    }\n\n    // ...\n}\n```\n\nIf the required logic for the `get` method is not defined, a `LogicError` will be thrown.\n\n### `update` method\n\nThe `update` method is used to update details of the specified item:\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\n\n$depot = new UserDepot;\n\n/** @var array\u003cstring, mixed\u003e */\n$data = /** ... */;\n\n$depot-\u003eupdate(99, $data);\n```\n\nWhen using the `update` method, its required logic must also be defined:\n\n``` php\nnamespace Acme\\Depots;\n\nuse Acme\\Sample\\UserUpdater;\nuse Rougin\\Dexterity\\Depot;\n\nclass UserDepot extends Depot\n{\n    // ...\n\n    /**\n     * Updates the specified item.\n     *\n     * @param integer              $id\n     * @param array\u003cstring, mixed\u003e $data\n     *\n     * @return boolean\n     */\n    public function update($id, $data)\n    {\n        return UserUpdater::update($id, $data);\n    }\n}\n```\n\nIf the logic for the `update` method is not defined, it will throw a `LogicError`.\n\n## Using `Route` traits\n\nThe `Route` traits in `Dexterity` is similar to the previously discussed `Depot` class. While the `Depot` class conforms to the [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete), the `Route` traits closely follows the [RESTful software architecture style](https://en.wikipedia.org/wiki/REST) and uses the [PSR-07](https://www.php-fig.org/psr/psr-7/) standard for standardization of its HTTP responses:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Acme\\Depots\\UserDepot;\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithIndexMethod;\n\nclass Users\n{\n    use WithIndexMethod;\n\n    protected $user;\n\n    public function __construct(UserDepot $user)\n    {\n        $this-\u003euser = $user;\n    }\n\n    protected function setIndexData($params)\n    {\n        $result = $this-\u003euser-\u003eget($params['page'], $params['limit']);\n\n        return new JsonResponse($result-\u003etoArray());\n    }\n}\n```\n\n``` php\n// index.php\n\nuse Acme\\Depots\\UserDepot;\nuse Acme\\Routes\\Users;\n\n$depot = new UserDepot;\n\n$route = new Users($depot);\n\n/** @var \\Psr\\Http\\Message\\ResponseInterface */\n$request = /** ... */\n\n$response = $route-\u003eindex($request);\n```\n\nFor each `Route` trait contains the following methods for writing their logic:\n\n**`is[METHOD]Valid`**\n\nThis trait method will be triggered if `[METHOD]` requires to be validated first. If not specified, it always return to `true` by default:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Route\\WithIndexMethod;\n\nclass Users\n{\n    use WithIndexMethod;\n\n    // ...\n\n    /**\n     * Checks if the items are allowed to be returned.\n     *\n     * @param array\u003cstring, mixed\u003e $params\n     *\n     * @return boolean\n     */\n    protected function isIndexValid($params)\n    {\n        return true;\n    }\n}\n```\n\n**`invalid[METHOD]`**\n\nThis trait method will be triggered if the `is[METHOD]Valid` trait method returns to `false`. This should return an HTTP response with an HTTP code between `4xx` to `5xx`:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\ErrorResponse;\nuse Rougin\\Dexterity\\Route\\WithIndexMethod;\n\nclass Users\n{\n    use WithIndexMethod;\n\n    // ...\n\n    /**\n     * Returns a response if the validation failed.\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function invalidIndex()\n    {\n        return new ErrorResponse(HttpResponse::UNPROCESSABLE);\n    }\n}\n```\n\n**`set[METHOD]Data`**\n\nThis is the main trait method that requires to write its logic based on `[METHOD]`:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithIndexMethod;\n\nclass Users\n{\n    use WithIndexMethod;\n\n    // ...\n\n    /**\n     * Executes the logic for returning an array of items.\n     *\n     * @param array\u003cstring, mixed\u003e $params\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function setIndexData($params)\n    {\n        $result = $this-\u003euser-\u003eget($params['page'], $params['limit']);\n\n        return new JsonResponse($result-\u003etoArray());\n    }\n}\n```\n\nUsing this kind of approach improves the code structure of HTTP routes as it only requires to write the logic for each `Route` trait being used (e.g., `WithIndexMethod`).\n\n\u003e [!NOTE]\n\u003e In other PHP frameworks and other guides, `Route` is also known as `Controller`.\n\n### `WithDeleteMethod` trait\n\nThe `WithDeleteMethod` trait adds a `delete` method in an HTTP route which can be used for deleting a specified item:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\ErrorResponse;\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithDeleteMethod;\n\nclass Users\n{\n    use WithDeleteMethod;\n\n    // ...\n\n    /**\n     * Returns a response if the validation failed.\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function invalidDelete()\n    {\n        return new ErrorResponse(HttpResponse::NOT_FOUND);\n    }\n\n    /**\n     * Checks if the specified item can be deleted.\n     *\n     * @param integer $id\n     *\n     * @return boolean\n     */\n    protected function isDeleteValid($id)\n    {\n        return true;\n    }\n\n    /**\n     * Executes the logic for deleting the specified item.\n     *\n     * @param integer $id\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function setDeleteData($id)\n    {\n        $this-\u003euser-\u003edelete($id);\n\n        return new JsonResponse('Deleted!', 204);\n    }\n}\n```\n\n``` php\n// index.php\n\n// ...\n\n/** @var \\Acme\\Depots\\UserDepot */\n$route = /** ... */;\n\n$response = $route-\u003edelete($id);\n```\n\n### `WithIndexMethod` trait\n\nThe `WithIndexMethod` trait allows to use the `index` method from an HTTP route. The specified method should return an array of items as its HTTP response:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\ErrorResponse;\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithIndexMethod;\n\nclass Users\n{\n    use WithIndexMethod;\n\n    // ...\n\n    /**\n     * Returns a response if the validation failed.\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function invalidIndex()\n    {\n        return new ErrorResponse(HttpResponse::UNPROCESSABLE);\n    }\n\n    /**\n     * Checks if the items are allowed to be returned.\n     *\n     * @param array\u003cstring, mixed\u003e $params\n     *\n     * @return boolean\n     */\n    protected function isIndexValid($params)\n    {\n        return true;\n    }\n\n    /**\n     * Executes the logic for returning an array of items.\n     *\n     * @param array\u003cstring, mixed\u003e $params\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function setIndexData($params)\n    {\n        $result = $this-\u003euser-\u003eget($params['page'], $params['limit']);\n\n        return new JsonResponse($result-\u003etoArray());\n    }\n}\n```\n\n``` php\n// index.php\n\n// ...\n\n/** @var \\Psr\\Http\\Message\\ResponseInterface */\n$request = /** ... */\n\n/** @var \\Acme\\Depots\\UserDepot */\n$route = /** ... */;\n\n$response = $route-\u003eindex($request);\n```\n\n### `WithShowMethod` trait\n\nThis `Route` trait allows to use the `show` method which returns an HTTP response for the specified item:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\ErrorResponse;\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithShowMethod;\n\nclass Users\n{\n    use WithShowMethod;\n\n    // ...\n\n    /**\n     * Returns a response if the validation failed.\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function invalidShow()\n    {\n        return new ErrorResponse(HttpResponse::UNPROCESSABLE);\n    }\n\n    /**\n     * Checks if the specified item is allowed to be returned.\n     *\n     * @param integer $id\n     * @param array\u003cstring, mixed\u003e $params\n     *\n     * @return boolean\n     */\n    protected function isShowValid($id, $params)\n    {\n        return true;\n    }\n\n    /**\n     * Executes the logic for returning the specified item.\n     *\n     * @param integer              $id\n     * @param array\u003cstring, mixed\u003e $params\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function setShowData($id, $params)\n    {\n        $item = $this-\u003euser-\u003efind($id);\n\n        return new JsonResponse($item);\n    }\n}\n```\n\n``` php\n// index.php\n\n// ...\n\n/** @var \\Psr\\Http\\Message\\ResponseInterface */\n$request = /** ... */\n\n/** @var \\Acme\\Depots\\UserDepot */\n$route = /** ... */;\n\n$response = $route-\u003eshow(99, $request);\n```\n\n### `WithStoreMethod` trait\n\nThis trait enables the specified HTTP route to use the `store` method. The specified method should be responsible for creating new items to the specified storage:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\ErrorResponse;\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithStoreMethod;\n\nclass Users\n{\n    use WithStoreMethod;\n\n    // ...\n\n    /**\n     * Returns a response if the validation failed.\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function invalidStore()\n    {\n        return new ErrorResponse(HttpResponse::UNPROCESSABLE);\n    }\n\n    /**\n     * Checks if it is allowed to create a new item.\n     *\n     * @param array\u003cstring, mixed\u003e $parsed\n     *\n     * @return boolean\n     */\n    protected function isStoreValid($parsed)\n    {\n        return true;\n    }\n\n    /**\n     * Executes the logic for creating a new item.\n     *\n     * @param array\u003cstring, mixed\u003e $parsed\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function setStoreData($parsed)\n    {\n        $this-\u003euser-\u003ecreate($parsed);\n\n        return new JsonResponse('Created!', 201);\n    }\n}\n```\n\n``` php\n// index.php\n\n// ...\n\n/** @var \\Psr\\Http\\Message\\ResponseInterface */\n$request = /** ... */\n\n/** @var \\Acme\\Depots\\UserDepot */\n$route = /** ... */;\n\n$response = $route-\u003estore($request);\n```\n\n### `WithUpdateMethod` trait\n\nThe `WithUpdateMethod` trait adds an `update` method to an HTTP route which updates the details of the specified item:\n\n``` php\nnamespace Acme\\Routes;\n\nuse Rougin\\Dexterity\\Message\\ErrorResponse;\nuse Rougin\\Dexterity\\Message\\JsonResponse;\nuse Rougin\\Dexterity\\Route\\WithUpdateMethod;\n\nclass Users\n{\n    use WithUpdateMethod;\n\n    // ...\n\n    /**\n     * Returns a response if the validation failed.\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function invalidUpdate()\n    {\n        return new ErrorResponse(HttpResponse::UNPROCESSABLE);\n    }\n\n    /**\n     * Checks if the specified item can be updated.\n     *\n     * @param integer $id\n     * @param array\u003cstring, mixed\u003e $parsed\n     *\n     * @return boolean\n     */\n    protected function isUpdateValid($id, $parsed)\n    {\n        return true;\n    }\n\n    /**\n     * Executes the logic for updating the specified item.\n     *\n     * @param integer              $id\n     * @param array\u003cstring, mixed\u003e $parsed\n     *\n     * @return \\Psr\\Http\\Message\\ResponseInterface\n     */\n    protected function setUpdateData($id, $parsed)\n    {\n        $this-\u003euser-\u003eupdate($id, $parsed);\n\n        return new JsonResponse('Updated!', 204);\n    }\n}\n```\n\n``` php\n// index.php\n\n// ...\n\n/** @var \\Psr\\Http\\Message\\ResponseInterface */\n$request = /** ... */\n\n/** @var \\Acme\\Depots\\UserDepot */\n$route = /** ... */;\n\n$response = $route-\u003eupdate(99, $request);\n```\n\n## Changelog\n\nPlease see [CHANGELOG][link-changelog] for more information what has changed recently.\n\n## Testing\n\nIf there is a need to check the source code of `Dexterity` for development purposes (e.g., creating fixes, new features, etc.), kindly clone this repository first to a local machine:\n\n``` bash\n$ git clone https://github.com/rougin/dexterity.git \"Sample\"\n```\n\nAfter cloning, use `Composer` to install its required packages:\n\n``` bash\n$ cd Sample\n$ composer update\n```\n\n\u003e [!NOTE]\n\u003e Please see also the [build.yml](https://github.com/rougin/dexterity/blob/master/.github/workflows/build.yml) of `Dexterity` to check any packages that needs to be installed based on the PHP version.\n\n## License\n\nThe MIT License (MIT). Please see [LICENSE][link-license] for more information.\n\n[ico-build]: https://img.shields.io/github/actions/workflow/status/rougin/dexterity/build.yml?style=flat-square\n[ico-coverage]: https://img.shields.io/codecov/c/github/rougin/dexterity?style=flat-square\n[ico-downloads]: https://img.shields.io/packagist/dt/rougin/dexterity.svg?style=flat-square\n[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square\n[ico-version]: https://img.shields.io/packagist/v/rougin/dexterity.svg?style=flat-square\n\n[link-build]: https://github.com/rougin/dexterity/actions\n[link-changelog]: https://github.com/rougin/dexterity/blob/master/CHANGELOG.md\n[link-contributors]: https://github.com/rougin/dexterity/contributors\n[link-coverage]: https://app.codecov.io/gh/rougin/dexterity\n[link-downloads]: https://packagist.org/packages/rougin/dexterity\n[link-license]: https://github.com/rougin/dexterity/blob/master/LICENSE.md\n[link-packagist]: https://packagist.org/packages/rougin/dexterity\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frougin%2Fdexterity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frougin%2Fdexterity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frougin%2Fdexterity/lists"}