{"id":13834502,"url":"https://github.com/woohoolabs/yin","last_synced_at":"2026-01-06T04:56:34.277Z","repository":{"id":33922635,"uuid":"37643398","full_name":"woohoolabs/yin","owner":"woohoolabs","description":"The efficient and elegant JSON:API 1.1 server library for PHP","archived":false,"fork":false,"pushed_at":"2023-08-23T13:40:19.000Z","size":2004,"stargazers_count":239,"open_issues_count":5,"forks_count":38,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-11-20T20:38:45.742Z","etag":null,"topics":["json-api","php","psr-7","server","transformer"],"latest_commit_sha":null,"homepage":"","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/woohoolabs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":"SUPPORT.md","governance":null,"roadmap":null,"authors":null}},"created_at":"2015-06-18T07:05:02.000Z","updated_at":"2024-09-27T21:10:10.000Z","dependencies_parsed_at":"2024-01-23T11:58:39.604Z","dependency_job_id":null,"html_url":"https://github.com/woohoolabs/yin","commit_stats":{"total_commits":806,"total_committers":24,"mean_commits":"33.583333333333336","dds":"0.048387096774193505","last_synced_commit":"1ef5f60273c674a42d56ef5c61f562d9f3e45a52"},"previous_names":[],"tags_count":62,"template":false,"template_full_name":null,"purl":"pkg:github/woohoolabs/yin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woohoolabs%2Fyin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woohoolabs%2Fyin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woohoolabs%2Fyin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woohoolabs%2Fyin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/woohoolabs","download_url":"https://codeload.github.com/woohoolabs/yin/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woohoolabs%2Fyin/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264545157,"owners_count":23625403,"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":["json-api","php","psr-7","server","transformer"],"created_at":"2024-08-04T14:00:35.883Z","updated_at":"2026-01-06T04:56:34.237Z","avatar_url":"https://github.com/woohoolabs.png","language":"PHP","funding_links":[],"categories":["Table of Contents"],"sub_categories":["Zend Framework 3"],"readme":"# Woohoo Labs. Yin\n\n[![Latest Version on Packagist][ico-version]][link-version]\n[![Software License][ico-license]](LICENSE)\n[![Build Status][ico-build]][link-build]\n[![Coverage Status][ico-coverage]][link-coverage]\n[![Quality Score][ico-code-quality]][link-code-quality]\n[![Total Downloads][ico-downloads]][link-downloads]\n[![Gitter][ico-support]][link-support]\n\n**Woohoo Labs. Yin is a PHP framework which helps you to build beautifully crafted JSON:APIs.**\n\n## Table of Contents\n\n* [Introduction](#introduction)\n    * [Features](#features)\n    * [Why Yin?](#why-yin)\n* [Install](#install)\n* [Basic Usage](#basic-usage)\n    * [Documents](#documents)\n    * [Resources](#resources)\n    * [Hydrators](#hydrators)\n    * [Exceptions](#exceptions)\n    * [JsonApi class](#jsonapi-class)\n    * [JsonApiRequest class](#jsonapirequest-class)\n* [Advanced Usage](#advanced-usage)\n    * [Pagination](#pagination)\n    * [Loading relationship data efficiently](#loading-relationship-data-efficiently)\n    * [Injecting metadata into documents](#injecting-metadata-into-documents)\n    * [Content negotiation](#content-negotiation)\n    * [Request/response validation](#requestresponse-validation)\n    * [Custom serialization](#custom-serialization)\n    * [Custom deserialization](#custom-deserialization)\n    * [Middleware](#middleware)\n* [Examples](#examples)\n    * [Fetching a single resource](#fetching-a-single-resource)\n    * [Fetching a collection of resources](#fetching-a-collection-of-resources)\n    * [Fetching a relationship](#fetching-a-relationship)\n    * [Creating a new resource](#creating-a-new-resource)\n    * [Updating a resource](#updating-a-resource)\n    * [How to try it out](#how-to-try-it-out)\n* [Integrations](#integrations)\n* [Versioning](#versioning)\n* [Change Log](#change-log)\n* [Testing](#testing)\n* [Contributing](#contributing)\n* [Support](#support)\n* [Credits](#credits)\n* [License](#license)\n\n## Introduction\n\n[JSON:API](https://jsonapi.org) specification\n[reached 1.0 on 29th May 2015](https://www.programmableweb.com/news/new-json-api-specification-aims-to-speed-api-development/2015/06/10)\nand we also believe it is a big day for RESTful APIs as this specification can help you make APIs more robust and\nfuture-proof. Woohoo Labs. Yin (named after Yin-Yang) was born to bring efficiency and elegance to your JSON:API\nservers, while [Woohoo Labs. Yang](https://github.com/woohoolabs/yang) is its client-side counterpart.\n\n### Features\n\n- 100% [PSR-7](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md) compatibility\n- 99% [JSON:API 1.1](https://jsonapi.org/) compatibility (approximately)\n- Developed for efficiency and ease of use\n- Extensive documentation and examples\n- Provides Documents and Transformers to fetch resources\n- Provides Hydrators to create and update resources\n- [Additional middleware](https://github.com/woohoolabs/yin-middleware) for the easier kickstart and debugging\n\n### Why Yin?\n\n#### Complete JSON:API framework\n\nWoohoo Labs. Yin is a framework-agnostic library which supports the vast majority of the JSON:API 1.1 specification:\nit provides various capabilities including content negotiation, error handling and pagination, as well as fetching,\ncreation, updating and deleting resources. Although Yin consists of many loosely coupled packages and classes which\ncan be used separately, the framework is most powerful when used in its entirety.\n\n#### Efficiency\n\nWe designed Yin to be as efficient as possible. That's why attributes and relationships are transformed only and if\nonly they are requested. This feature is extremely advantageous when there are a lot of resources to transform or a\nrarely required transformation is very expensive. Furthermore, as transformers are stateless, the overhead of having a\nseparate model object for each resource is avoided. Additionally, due to statelessness, the overall library works really\nwell with dependency injection.\n\n#### Supplementary middleware\n\nThere is some [additional middleware](https://github.com/woohoolabs/yin-middleware) for Woohoo Labs. Yin you might\nfind useful. It can facilitate various tasks like error handling (via transformation of exceptions into JSON:API\nerror responses), dispatching JSON:API-aware controllers or debugging (via syntax checking and validation of requests\nand responses).\n\n## Install\n\nThe only thing you need before getting started is [Composer](https://getcomposer.org).\n\n### Install a PSR-7 implementation:\n\nBecause Yin requires a PSR-7 implementation (a package which provides the `psr/http-message-implementation` virtual\npackage), you must install one first. You may use [Laminas Diactoros](https://github.com/laminas/laminas-diactoros) or\nany other library of your preference:\n\n```bash\n$ composer require laminas/laminas-diactoros\n```\n\n### Install Yin:\n\nTo install the latest version of this library, run the command below:\n\n```bash\n$ composer require woohoolabs/yin\n```\n\n\u003e Note: The tests and examples won't be downloaded by default. You have to use `composer require woohoolabs/yin --prefer-source`\nor clone the repository if you need them.\n\nThe latest version of Yin requires PHP 7.1 at least but you can use Yin 2.0.6 for PHP 7.0.\n\n### Install the optional dependencies:\n\nIf you want to take advantage of request/response validation then you have to also ask for the following\ndependencies:\n\n```bash\n$ composer require justinrainbow/json-schema\n$ composer require seld/jsonlint\n```\n\n## Basic Usage\n\nWhen using Woohoo Labs. Yin, you will create:\n- Documents and resources in order to map domain objects to JSON:API responses\n- Hydrators in order to transform resources in a POST or PATCH request to domain objects\n\nFurthermore, a [`JsonApi`](#jsonapi-class) class will be responsible for the instrumentation, while a PSR-7 compatible\n[`JsonApiRequest`](#jsonapirequest-class) class provides functionalities you commonly need.\n\n### Documents\n\nThe following sections will guide you through creating documents for successful responses and\ncreating or building error documents.\n\n#### Documents for successful responses\n\nFor successful requests, you must return information about one or more resources. Woohoo Labs. Yin provides\nmultiple abstract classes that help you to create your own documents for different use cases:\n\n- `AbstractSuccessfulDocument`: A generic base document for successful responses\n- `AbstractSimpleResourceDocument`: A base class for documents about a single, very simple top-level resource\n- `AbstractSingleResourceDocument`: A base class for documents about a single, more complex top-level resource\n- `AbstractCollectionDocument`: A base class for documents about a collection of top-level resources\n\nAs the `AbstractSuccessfulDocument` is only useful for special use-cases (e.g. when a document can contain resources\nof multiple types), we will not cover it here.\n\nThe difference between the `AbstractSimpleResourceDocument` and the `AbstractSingleResourceDocument` classes is that\nthe first one doesn't need a [resource](#resource) object. For this reason, it is preferable to use\nthe former for only really simple domain objects (like messages), while the latter works better for more complex domain\nobjects (like users or addresses).\n\nLet's first have a quick look at the `AbstractSimpleResourceDocument`: it has a `getResource()` abstract method which\nneeds to be implemented when you extend this class. The `getResource()` method returns the whole transformed resource as\nan array including the type, id, attributes, and relationships like below:\n\n```php\nprotected function getResource(): array\n{\n    return [\n        \"type\"       =\u003e \"\u003ctype\u003e\",\n        \"id\"         =\u003e \"\u003cID\u003e\",\n        \"attributes\" =\u003e [\n            \"key\" =\u003e \"value\",\n        ],\n    ];\n}\n```\n\n\u003e Please note that `AbstractSimpleResourceDocument` doesn't support some features out-of-the-box like sparse fieldsets,\nautomatic inclusion of related resources etc. That's why this document type should only be considered as a quick-and-dirty\nsolution, and generally you should choose another, more advanced document type introduced below in the majority of the use cases.\n\n`AbstractSingleResourceDocument` and `AbstractCollectionDocument` both need a [resource](#resource) object in order to work,\nwhich is a concept introduced in the following sections. For now, it is enough to know that one must be passed for the documents\nduring instantiation. This means that a minimal constructor of your documents should look like this:\n\n```php\npublic function __construct(MyResource $resource)\n{\n    parent::__construct($resource);\n}\n```\n\nYou can of course provide other dependencies for your constructor or completely omit it if you don't need it.\n\nWhen you extend either `AbstractSingleResourceDocument` or `AbstractCollectionDocument`, they both require\nyou to implement the following methods:\n\n```php\n/**\n * Provides information about the \"jsonapi\" member of the current document.\n *\n * The method returns a new JsonApiObject object if this member should be present or null\n * if it should be omitted from the response.\n */\npublic function getJsonApi(): ?JsonApiObject\n{\n    return new JsonApiObject(\"1.1\");\n}\n```\n\nThe description says it very clear: if you want a `jsonapi` member in your response, then create a new `JsonApiObject`.\nIts constructor expects the JSON:API version number and an optional meta object (as an array).\n\n```php\n/**\n * Provides information about the \"meta\" member of the current document.\n *\n * The method returns an array of non-standard meta information about the document. If\n * this array is empty, the member won't appear in the response.\n */\npublic function getMeta(): array\n{\n    return [\n        \"page\" =\u003e [\n            \"offset\" =\u003e $this-\u003eobject-\u003egetOffset(),\n            \"limit\" =\u003e $this-\u003eobject-\u003egetLimit(),\n            \"total\" =\u003e $this-\u003eobject-\u003egetCount(),\n        ]\n    ];\n}\n```\n\nDocuments may also have a \"meta\" member which can contain any non-standard information. The example above adds\ninformation about pagination to the document.\n\nNote that the `object` property is a variable of any type (in this case it is a hypothetical collection),\nand this is the main \"subject\" of the document.\n\n```php\n/**\n * Provides information about the \"links\" member of the current document.\n *\n * The method returns a new DocumentLinks object if you want to provide linkage data\n * for the document or null if the member should be omitted from the response.\n */\npublic function getLinks(): ?DocumentLinks\n{\n    return new DocumentLinks(\n        \"https://example.com/api\",\n        [\n            \"self\" =\u003e new Link(\"/books/\" . $this-\u003egetResourceId())\n        ]\n    );\n\n    /* This is equivalent to the following:\n    return DocumentLinks::createWithBaseUri(\n        \"https://example.com/api\",\n        [\n            \"self\" =\u003e new Link(\"/books/\" . $this-\u003egetResourceId())\n        ]\n    );\n}\n```\n\nThis time, we want a self link to appear in the document. For this purpose, we utilize the `getResourceId()` method,\nwhich is a shortcut of calling the resource (which is introduced below) to obtain the ID of the\nprimary resource (`$this-\u003eresource-\u003egetId($this-\u003eobject)`).\n\nThe only difference between the `AbstractSingleResourceDocument` and `AbstractCollectionDocument` is the way they\nregard the `object`. The first one regards it as a single domain object while the latter regards it\nas an iterable collection.\n\n##### Usage\n\nDocuments can be transformed to HTTP responses. The easiest way to achieve this is to use the\n[`JsonApi` class](#jsonapi-class) and choose the appropriate response type. Successful documents support three\nkinds of responses:\n\n- normal: All the top-level members can be present in the response (except for the \"errors\")\n- meta: Only the \"jsonapi\", \"links\" and meta top-level member can be present in the response\n- relationship: The specified relationship object will be the primary data of the response\n\n#### Documents for error responses\n\nAn `AbstractErrorDocument` can be used to create reusable documents for error responses. It also requires the same\nabstract methods to be implemented as the successful ones, but additionally an `addError()` method can be used\nto include error items.\n\n```php\n/** @var AbstractErrorDocument $errorDocument */\n$errorDocument = new MyErrorDocument();\n$errorDocument-\u003eaddError(new MyError());\n```\n\nThere is an `ErrorDocument` too, which makes it possible to build error responses on-the-fly:\n\n```php\n/** @var ErrorDocument $errorDocument */\n$errorDocument = new ErrorDocument();\n$errorDocument-\u003esetJsonApi(new JsonApiObject(\"1.0\"));\n$errorDocument-\u003esetLinks(ErrorLinks::createWithoutBaseUri()-\u003esetAbout(\"https://example.com/api/errors/404\")));\n$errorDocument-\u003eaddError(new MyError());\n```\n\n### Resources\n\nDocuments for successful responses can contain one or more top-level resources and included resources.\nThat's why resources are responsible for converting domain objects into JSON:API resources and resource\nidentifiers.\n\nAlthough you are encouraged to create one transformer for each resource type, you also have the ability to define\n\"composite\" resources following the Composite design pattern.\n\nResources must implement the `ResourceInterface`. In order to facilitate this job, you can also extend the\n`AbstractResource` class.\n\nChildren of the `AbstractResource` class need several abstract methods to be implemented - most of them are similar to\nthe ones seen in the Document objects. The following example illustrates a resource dealing with a book domain object\nand its \"authors\" and \"publisher\" relationships.\n\n```php\nclass BookResource extends AbstractResource\n{\n    /**\n     * @var AuthorResource\n     */\n    private $authorResource;\n\n    /**\n     * @var PublisherResource\n     */\n    private $publisherResource;\n\n    /**\n     * You can type-hint the object property this way.\n     * @var array\n     */\n    protected $object;\n\n    public function __construct(\n        AuthorResource $authorResource,\n        PublisherResource $publisherResource\n    ) {\n        $this-\u003eauthorResource = $authorResource;\n        $this-\u003epublisherResource = $publisherResource;\n    }\n\n    /**\n     * Provides information about the \"type\" member of the current resource.\n     *\n     * The method returns the type of the current resource.\n     *\n     * @param array $book\n     */\n    public function getType($book): string\n    {\n        return \"book\";\n    }\n\n    /**\n     * Provides information about the \"id\" member of the current resource.\n     *\n     * The method returns the ID of the current resource which should be a UUID.\n     *\n     * @param array $book\n     */\n    public function getId($book): string\n    {\n        return $this-\u003eobject[\"id\"];\n\n        // This is equivalent to the following (the $book parameter is used this time instead of $this-\u003eobject):\n        return $book[\"id\"];\n    }\n\n    /**\n     * Provides information about the \"meta\" member of the current resource.\n     *\n     * The method returns an array of non-standard meta information about the resource. If\n     * this array is empty, the member won't appear in the response.\n     *\n     * @param array $book\n     */\n    public function getMeta($book): array\n    {\n        return [];\n    }\n\n    /**\n     * Provides information about the \"links\" member of the current resource.\n     *\n     * The method returns a new ResourceLinks object if you want to provide linkage\n     * data about the resource or null if it should be omitted from the response.\n     *\n     * @param array $book\n     */\n    public function getLinks($book): ?ResourceLinks\n    {\n        return new ResourceLinks::createWithoutBaseUri()-\u003esetSelf(new Link(\"/books/\" . $this-\u003egetId($book)));\n\n        // This is equivalent to the following:\n        // return new ResourceLinks(\"\", new Link(\"/books/\" . $this-\u003egetResourceId()));\n    }\n\n    /**\n     * Provides information about the \"attributes\" member of the current resource.\n     *\n     * The method returns an array where the keys signify the attribute names,\n     * while the values are callables receiving the domain object as an argument,\n     * and they should return the value of the corresponding attribute.\n     *\n     * @param array $book\n     * @return callable[]\n     */\n    public function getAttributes($book): array\n    {\n        return [\n            \"title\" =\u003e function () {\n                return $this-\u003eobject[\"title\"];\n            },\n            \"pages\" =\u003e function () {\n                return (int) $this-\u003eobject[\"pages\"];\n            },\n        ];\n\n        // This is equivalent to the following (the $book parameter is used this time instead of $this-\u003eobject):\n        return [\n            \"title\" =\u003e function (array $book) {\n                return $book[\"title\"];\n            },\n            \"pages\" =\u003e function (array $book) {\n                return (int) $book[\"pages\"];\n            },\n        ];\n    }\n\n    /**\n     * Returns an array of relationship names which are included in the response by default.\n     *\n     * @param array $book\n     */\n    public function getDefaultIncludedRelationships($book): array\n    {\n        return [\"authors\"];\n    }\n\n    /**\n     * Provides information about the \"relationships\" member of the current resource.\n     *\n     * The method returns an array where the keys signify the relationship names,\n     * while the values are callables receiving the domain object as an argument,\n     * and they should return a new relationship instance (to-one or to-many).\n     *\n     * @param array $book\n     * @return callable[]\n     */\n    public function getRelationships($book): array\n    {\n        return [\n            \"authors\" =\u003e function () {\n                return ToManyRelationship::create()\n                    -\u003esetLinks(\n                        RelationshipLinks::createWithoutBaseUri()-\u003esetSelf(new Link(\"/books/relationships/authors\"))\n                    )\n                    -\u003esetData($this-\u003eobject[\"authors\"], $this-\u003eauthorTransformer);\n            },\n            \"publisher\" =\u003e function () {\n                return ToOneRelationship::create()\n                    -\u003esetLinks(\n                        RelationshipLinks::createWithoutBaseUri()-\u003esetSelf(new Link(\"/books/relationships/publisher\"))\n                    )\n                    -\u003esetData($this-\u003eobject[\"publisher\"], $this-\u003epublisherTransformer);\n            },\n        ];\n\n        // This is equivalent to the following (the $book parameter is used this time instead of $this-\u003eobject):\n\n        return [\n            \"authors\" =\u003e function (array $book) {\n                return ToManyRelationship::create()\n                    -\u003esetLinks(\n                        RelationshipLinks::createWithoutBaseUri()-\u003esetSelf(new Link(\"/books/relationships/authors\"))\n                    )\n                    -\u003esetData($book[\"authors\"], $this-\u003eauthorTransformer);\n            },\n            \"publisher\" =\u003e function ($book) {\n                return ToOneRelationship::create()\n                    -\u003esetLinks(\n                        RelationshipLinks::createWithoutBaseUri()-\u003esetSelf(new Link(\"/books/relationships/publisher\"))\n                    )\n                    -\u003esetData($book[\"publisher\"], $this-\u003epublisherTransformer);\n            },\n        ];\n    }\n}\n```\n\nGenerally, you don't use resources directly. Only documents need them to be able to fill the \"data\", the \"included\",\nand the \"relationship\" members in the responses.\n\n### Hydrators\n\nHydrators allow us to initialize the properties of a domain object as required by the current HTTP request. This means,\nwhen a client wants to create or update a resource, hydrators can help instantiate a domain object, which can then be\nvalidated, saved etc.\n\nThere are three abstract hydrator classes in Woohoo Labs. Yin:\n\n- `AbstractCreateHydrator`: It can be used for requests which create a new resource\n- `AbstractUpdateHydrator`: It can be used for requests which update an existing resource\n- `AbstractHydrator`: It can be used for both type of requests\n\nFor the sake of brevity, we only introduce the usage of the latter class as it is simply the union of\n`AbstractCreateHydrator` and `AbstractUpdateHydrator`. Let's have a look at an example hydrator:\n\n```php\nclass BookHydrator extends AbstractHydrator\n{\n    /**\n     * Determines which resource types can be accepted by the hydrator.\n     *\n     * The method should return an array of acceptable resource types. When such a resource is received for hydration\n     * which can't be accepted (its type doesn't match the acceptable types of the hydrator), a ResourceTypeUnacceptable\n     * exception will be raised.\n     *\n     * @return string[]\n     */\n    protected function getAcceptedTypes(): array\n    {\n        return [\"book\"];\n    }\n\n    /**\n     * Validates a client-generated ID.\n     *\n     * If the $clientGeneratedId is not a valid ID for the domain object, then\n     * the appropriate exception should be thrown: if it is not well-formed then\n     * a ClientGeneratedIdNotSupported exception can be raised, if the ID already\n     * exists then a ClientGeneratedIdAlreadyExists exception can be thrown.\n     *\n     * @throws ClientGeneratedIdNotSupported\n     * @throws ClientGeneratedIdAlreadyExists\n     * @throws Exception\n     */\n    protected function validateClientGeneratedId(\n        string $clientGeneratedId,\n        JsonApiRequestInterface $request,\n        ExceptionFactoryInterface $exceptionFactory\n    ) {\n        if ($clientGeneratedId !== null) {\n            throw $exceptionFactory-\u003ecreateClientGeneratedIdNotSupportedException($request, $clientGeneratedId);\n        }\n    }\n\n    /**\n     * Produces a new ID for the domain objects.\n     *\n     * UUID-s are preferred according to the JSON:API specification.\n     */\n    protected function generateId(): string\n    {\n        return Uuid::generate();\n    }\n\n    /**\n     * Sets the given ID for the domain object.\n     *\n     * The method mutates the domain object and sets the given ID for it.\n     * If it is an immutable object or an array the whole, updated domain\n     * object can be returned.\n     *\n     * @param array $book\n     * @return mixed|void\n     */\n    protected function setId($book, string $id)\n    {\n        $book[\"id\"] = $id;\n\n        return $book;\n    }\n\n    /**\n     * You can validate the request.\n     *\n     * @throws JsonApiExceptionInterface\n     */\n    protected function validateRequest(JsonApiRequestInterface $request): void\n    {\n        // WARNING! THIS CONDITION CONTRADICTS TO THE SPEC\n        if ($request-\u003egetAttribute(\"title\") === null) {\n            throw new LogicException(\"The 'title' attribute is required!\");\n        }\n    }\n\n    /**\n     * Provides the attribute hydrators.\n     *\n     * The method returns an array of attribute hydrators, where a hydrator is a key-value pair:\n     * the key is the specific attribute name which comes from the request and the value is a\n     * callable which hydrates the given attribute.\n     * These callables receive the domain object (which will be hydrated), the value of the\n     * currently processed attribute, the \"data\" part of the request and the name of the attribute\n     * to be hydrated as their arguments, and they should mutate the state of the domain object.\n     * If it is an immutable object or an array (and passing by reference isn't used),\n     * the callable should return the domain object.\n     *\n     * @param array $book\n     * @return callable[]\n     */\n    protected function getAttributeHydrator($book): array\n    {\n        return [\n            \"title\" =\u003e function (array $book, $attribute, $data, $attributeName) {\n                $book[\"title\"] = $attribute;\n\n                return $book;\n            },\n            \"pages\" =\u003e function (array \u0026$book, $attribute, $data, $attributeName) {\n                $book[\"pages\"] = $attribute;\n            },\n        ];\n    }\n\n    /**\n     * Provides the relationship hydrators.\n     *\n     * The method returns an array of relationship hydrators, where a hydrator is a key-value pair:\n     * the key is the specific relationship name which comes from the request and the value is a\n     * callable which hydrate the previous relationship.\n     * These callables receive the domain object (which will be hydrated), an object representing the\n     * currently processed relationship (it can be a ToOneRelationship or a ToManyRelationship\n     * object), the \"data\" part of the request and the relationship name as their arguments, and\n     * they should mutate the state of the domain object.\n     * If it is an immutable object or an array (and passing by reference isn't used),\n     * the callable should return the domain object.\n     *\n     * @param mixed $book\n     * @return callable[]\n     */\n    protected function getRelationshipHydrator($book): array\n    {\n        return [\n            \"authors\" =\u003e function (array $book, ToManyRelationship $authors, $data, string $relationshipName) {\n                $book[\"authors\"] = BookRepository::getAuthors($authors-\u003egetResourceIdentifierIds());\n\n                return $book;\n            },\n            \"publisher\" =\u003e function (array \u0026$book, ToOneRelationship $publisher, $data, string $relationshipName) {\n                $book[\"publisher\"] = BookRepository::getPublisher($publisher-\u003egetResourceIdentifier()-\u003egetId());\n            },\n        ];\n    }\n\n    /**\n     * You can validate the domain object after it has been hydrated from the request.\n     * @param mixed $book\n     */\n    protected function validateDomainObject($book): void\n    {\n        if (empty($book[\"authors\"])) {\n            throw new LogicException(\"The 'authors' relationship cannot be empty!\");\n        }\n    }\n}\n```\n\nAccording to the [book example](examples/Book), the following request:\n\n```http\nPOST /books HTTP/1.1\nContent-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n\n{\n  \"data\": {\n    \"type\": \"book\",\n    \"attributes\": {\n      \"title\": \"Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation\",\n      \"pages\": 512\n    },\n    \"relationships\": {\n      \"authors\": {\n        \"data\": [\n            { \"type\": \"author\", \"id\": \"100\" },\n            { \"type\": \"author\", \"id\": \"101\" }\n        ]\n      }\n    }\n  }\n}\n```\n\nwill result in the following `Book` domain object:\n\n```\nArray\n(\n    [id] =\u003e 1\n    [title] =\u003e Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation\n    [pages] =\u003e 512\n    [authors] =\u003e Array\n        (\n            [0] =\u003e Array\n                (\n                    [id] =\u003e 100\n                    [name] =\u003e Jez Humble\n                )\n            [1] =\u003e Array\n                (\n                    [id] =\u003e 101\n                    [name] =\u003e David Farley\n                )\n        )\n    [publisher] =\u003e Array\n        (\n            [id] =\u003e 12346\n            [name] =\u003e Addison-Wesley Professional\n        )\n)\n```\n\n### Exceptions\n\nWoohoo Labs. Yin was designed to make error handling as easy and customizable as possible. That's why all the default\nexceptions extend the `JsonApiException` class and contain an [error document](#documents-for-error-responses) with the\nappropriate error object(s). That's why if you want to respond with an error document in case of an exception you\nneed to do the following:\n\n```php\ntry {\n    // Do something which results in an exception\n} catch (JsonApiExceptionInterface $e) {\n    // Get the error document from the exception\n    $errorDocument = $e-\u003egetErrorDocument();\n\n    // Instantiate the responder - make sure to pass the correct dependencies to it\n    $responder = Responder::create($request, $response, $exceptionFactory, $serializer);\n\n    // Create a response from the error document\n    $responder-\u003egenericError($errorDocument);\n\n    // Emit the HTTP response\n    sendResponse($response);\n}\n```\n\nTo guarantee total customizability, we introduced the concept of __Exception Factories__. These are classes\nwhich create all the exceptions thrown by Woohoo Labs. Yin. As an Exception Factory of __your own choice__ is passed to\nevery transformer and hydrator, you can completely customize what kind of exceptions are thrown.\n\nThe default [Exception Factory](https://github.com/woohoolabs/yin/blob/master/src/JsonApi/Exception/DefaultExceptionFactory)\ncreates children of [`JsonApiException`](src/JsonApi/Exception)s but you are free to create any `JsonApiExceptionInterface`\nexceptions. If you only want to customize the error document or the error objects of your exceptions, just extend the\nbasic `Exception` class and create your `createErrorDocument()` or `getErrors()` methods.\n\n### `JsonApi` class\n\nThe `JsonApi` class is the orchestrator of the whole framework. It is highly recommended to utilize this class\nif you want to use the entire functionality of Woohoo Labs. Yin. You can find various examples about the usage\nof it in the [examples section](#examples) or [example directory](https://github.com/woohoolabs/yin/blob/master/examples).\n\n### `JsonApiRequest` class\n\nThe `JsonApiRequest` class implements the `WoohooLabs\\Yin\\JsonApi\\Request\\JsonApiRequestInterface` which extends the PSR-7\n`ServerRequestInterface` with some useful, JSON:API related methods. For further information about the available methods,\nplease refer to the documentation of [`JsonApiRequestInterface`](https://github.com/woohoolabs/yin/blob/master/src/JsonApi/Request/JsonApiRequestInterface.php).\n\n## Advanced Usage\n\nThis section guides you through the advanced features of Yin.\n\n### Pagination\n\nYin is able to help you paginate your collection of resources. First, it provides some shortcuts for querying the\nrequest query parameters when page-based, offset-based, or cursor-based pagination strategies are used.\n\n#### Page-based pagination\n\nYin looks for the `page[number]` and the `page[size]` query parameters and parses their value. If any of them is missing\nthen the default page number or size will be used (\"1\" and \"10\" in the following example).\n\n```php\n$pagination = $jsonApi-\u003egetPaginationFactory()-\u003ecreatePageBasedPagination(1, 10);\n```\n\n#### Fixed page-based pagination\n\nYin looks for the `page[number]` query parameter and parses its value. If it is missing then the default page number\nwill be used (\"1\" in the following example). This strategy can be useful if you do not want to expose the page size\nat all.\n\n```php\n$pagination = $jsonApi-\u003egetPaginationFactory()-\u003ecreateFixedPageBasedPagination(1);\n```\n\n#### Offset-based pagination\n\nYin looks for the `page[offset]` and the `page[limit]` query parameters and parses their value. If any of them is missing\nthen the default offset or limit will be used (\"1\" and \"10\" in the following example).\n\n```php\n$pagination = $jsonApi-\u003egetPaginationFactory()-\u003ecreateOffsetBasedPagination(1, 10);\n```\n\n#### Cursor-based pagination\n\nYin looks for the `page[cursor]` and the `page[size]` query parameters and parses their value. If any of them is missing\nthen the default cursor or size will be used (\"2016-10-01\" or 10 in the following example).\n\n```php\n$pagination = $jsonApi-\u003egetPaginationFactory()-\u003ecreateCursorBasedPagination(\"2016-10-01\", 10);\n```\n\n#### Fixed cursor-based pagination\n\nYin looks for the `page[cursor]` query parameter and parses its value. If it is missing then the default cursor will\nbe used (\"2016-10-01\" in the following example).\n\n```php\n$pagination = $jsonApi-\u003egetPaginationFactory()-\u003ecreateFixedCursorBasedPagination(\"2016-10-01\");\n```\n\n#### Custom pagination\n\nIf you need a custom pagination strategy, you may use the `JsonApiRequestInterface::getPagination()` method which returns an\narray of pagination parameters.\n\n```php\n$paginationParams = $jsonApi-\u003egetRequest()-\u003egetPagination();\n\n$pagination = new CustomPagination($paginationParams[\"from\"] ?? 1, $paginationParams[\"to\"] ?? 1);\n```\n\n#### Usage\n\nAs soon as you have the appropriate pagination object, you may use them when you fetch your data from a data source:\n\n```\n$users = UserRepository::getUsers($pagination-\u003egetPage(), $pagination-\u003egetSize());\n```\n\n#### Pagination links\n\nThe JSON:API spec makes it available to provide pagination links for your resource collections. Yin is able to help you\nin this regard too. You have use the `DocumentLinks::setPagination()` method when you define links for your documents.\nIt expects the paginated URI and an object implementing the `PaginationLinkProviderInterface` as seen in the following\nexample:\n\n```php\npublic function getLinks(): ?DocumentLinks\n{\n    return DocumentLinks::createWithoutBaseUri()-\u003esetPagination(\"/users\", $this-\u003eobject);\n}\n```\n\nTo make things even easier, there are some `LinkProvider` traits in order to ease the development of\n`PaginationLinkProviderInterface` implementations of the built-in pagination strategies. For example a collection\nfor the `User` objects can use the `PageBasedPaginationLinkProviderTrait`. This way, only three abstract methods has\nto be implemented:\n\n```php\nclass UserCollection implements PaginationLinkProviderInterface\n{\n    use PageBasedPaginationLinkProviderTrait;\n\n    public function getTotalItems(): int\n    {\n        // ...\n    }\n\n    public function getPage(): int\n    {\n        // ...\n    }\n\n    public function getSize(): int\n    {\n        // ...\n    }\n\n    // ...\n}\n```\n\nYou can find the full example [here](https://github.com/woohoolabs/yin/blob/master/examples/Utils/Collection.php).\n\n### Loading relationship data efficiently\n\nSometimes it can be beneficial or necessary to fine-tune data retrieval of relationshipS. A possible scenario might be\nwhen you have a \"to-many\" relationship containing gazillion items. If this relationship isn't always needed than you\nmight only want to return a data key of a relationship when the relationship itself is included in the response. This\noptimization can save you bandwidth by omitting resource linkage.\n\nAn example is extracted from the [`UserResource`](https://github.com/woohoolabs/yin/blob/master/examples/User/JsonApi/Resource/UserResource.php)\nexample class:\n\n```php\npublic function getRelationships($user): array\n{\n    return [\n        \"contacts\" =\u003e function (array $user) {\n            return\n                ToManyRelationship::create()\n                    -\u003esetData($user[\"contacts\"], $this-\u003econtactTransformer)\n                    -\u003eomitDataWhenNotIncluded();\n        },\n    ];\n}\n```\n\nBy using the `omitDataWhenNotIncluded()` method, the relationship data will be omitted when the relationship is not\nincluded. However, sometimes this optimization is not enough on its own. Even though we can save bandwidth with the prior\ntechnique, the relationship still has to be loaded from the data source (probably from a database), because we pass it\nto the relationship object with the `setData()` method.\n\nThis problem can be mitigated by lazy-loading the relationship. To do so, you have to use `setDataAsCallable()` method\ninstead of `setData()`:\n\n```php\npublic function getRelationships($user): array\n{\n    return [\n        \"contacts\" =\u003e function (array $user) {\n            return\n                ToManyRelationship::create()\n                    -\u003esetDataAsCallable(\n                        function () use ($user) {\n                            // Lazily load contacts from the data source\n                            return $user-\u003eloadContactsFromDataSource();\n                        },\n                        $this-\u003econtactTransformer\n                    )\n                    -\u003eomitDataWhenNotIncluded()\n                ;\n        },\n    ];\n}\n```\n\nThis way, the contacts of a user will only be loaded when the given relationship's `data` key is present in the response,\nallowing your API to be as efficient as possible.\n\n### Injecting metadata into documents\n\nMetadata can be injected into documents on-the-fly. This comes handy if you want to customize or decorate your\nresponses. For example if you would like to inject a cache ID into the response document, you could use the following:\n\n```php\n// Calculate the cache ID\n$cacheId = calculateCacheId();\n\n// Respond with \"200 Ok\" status code along with the book document containing the cache ID in the meta data\nreturn $jsonApi-\u003erespond()-\u003eok($document, $book, [\"cache_id\" =\u003e $cacheId]);\n```\n\nUsually, the last argument of each responder method can be used to add meta data to your documents.\n\n### Content negotiation\n\nThe JSON:API standard specifies [some rules](#content-negotiation-servers) about content\nnegotiation. Woohoo Labs. Yin tries to help you enforce them with the `RequestValidator` class. Let's first create\na request validator to see it in action:\n\n```php\n$requestValidator = new RequestValidator(new DefaultExceptionFactory(), $includeOriginalMessageInResponse);\n```\n\nIn order to customize the exceptions which can be thrown, it is necessary to provide an [Exception Factory](#exceptions).\nOn the other hand, the `$includeOriginalMessageInResponse` argument can be useful in a development environment\nwhen you also want to return the original request body that triggered the exception in the error response.\n\nIn order to validate whether the current request's `Accept` and `Content-Type` headers conform to the JSON:API\nspecification, use this method:\n\n```php\n$requestValidator-\u003enegotiate($request);\n```\n\n### Request/response validation\n\nYou can use the following method to check if the query parameters of the current request are in line with\n[the naming rules](https://jsonapi.org/format/#query-parameters):\n\n```php\n$requestValidator-\u003evalidateQueryParams($request);\n```\n\n\u003e Note: In order to apply the following validations, remember to install the\n\u003e [optional dependencies](#install) of Yin.\n\nFurthermore, the request body can be validated if it is a well-formed JSON document:\n\n```php\n$requestValidator-\u003evalidateJsonBody($request);\n```\n\nSimilarly, responses can be validated too. Let's create a response validator first:\n\n```php\n$responseValidator = new ResponseValidator(\n    new JsonSerializer(),\n    new DefaultExceptionFactory(),\n    $includeOriginalMessageInResponse\n);\n```\n\nTo ensure that the response body is a well-formed JSON document, one can use the following method:\n\n```php\n$responseValidator-\u003evalidateJsonBody($response);\n```\n\nTo ensure that the response body is a well-formed JSON:API document, one can use the following method:\n\n```php\n$responseValidator-\u003evalidateJsonApiBody($response);\n```\n\nValidating the responses can be useful in a development environment to find possible bugs early.\n\n### Custom serialization\n\nYou can configure Yin to serialize responses in a custom way instead of using the default serializer (`JsonSerializer`)\nthat utilizes the `json_encode()` function to write JSON:API documents into the response body.\n\nIn the majority of the use-cases, the default serializer should be sufficient for your needs, but sometimes you might\nneed more sophistication. Or sometimes you want to do nasty things like returning your JSON:API response as an array\nwithout any serialization in case your API endpoint was called \"internally\".\n\nIn order to use a custom serializer, create a class implementing `SerializerInterface` and setup your `JsonApi`\ninstance accordingly (pay attention to the last argument):\n\n```php\n$jsonApi = new JsonApi(new JsonApiRequest(), new Response(), new DefaultExceptionFactory(), new CustomSerializer());\n```\n\n### Custom deserialization\n\nYou can configure Yin to deserialize requests in a custom way instead of using the default deserializer\n(`JsonDeserializer`) that utilizes the `json_decode()` function to parse the contents of the request body.\n\nIn the majority of the use-cases, the default deserializer should be sufficient for your needs, but sometimes you might\nneed more sophistication. Or sometimes you want to do nasty things like calling your JSON:API endpoints \"internally\"\nwithout converting your request body to JSON format.\n\nIn order to use a custom deserializer, create a class implementing `DeserializerInterface` and setup your `JsonApiRequest`\ninstance accordingly (pay attention to the last argument):\n\n```php\n$request = new JsonApiRequest(ServerRequestFactory::fromGlobals(), new DefaultExceptionFactory(), new CustomDeserializer());\n```\n\n### Middleware\n\nIf you use a middleware-oriented framework (like [Woohoo Labs. Harmony](https://github.com/woohoolabs/harmony),\n[Zend-Stratigility](https://github.com/zendframework/zend-stratigility/),\n[Zend-Expressive](https://github.com/zendframework/zend-expressive/) or\n[Slim Framework 3](https://www.slimframework.com/)), you will find the\n[Yin-middleware](https://github.com/woohoolabs/yin-middleware) library quite useful. Read the documentation to\nlearn about its advantages!\n\n## Examples\n\n### Fetching a single resource\n\n```php\npublic function getBook(JsonApi $jsonApi): ResponseInterface\n{\n    // Getting the \"id\" of the currently requested book\n    $id = $jsonApi-\u003egetRequest()-\u003egetAttribute(\"id\");\n\n    // Retrieving a book domain object with an ID of $id\n    $book = BookRepository::getBook($id);\n\n    // Instantiating a book document\n    $document = new BookDocument(\n        new BookResource(\n            new AuthorResource(),\n            new PublisherResource()\n        )\n    );\n\n    // Responding with \"200 Ok\" status code along with the book document\n    return $jsonApi-\u003erespond()-\u003eok($document, $book);\n}\n```\n\n### Fetching a collection of resources\n\n```php\npublic function getUsers(JsonApi $jsonApi): ResponseInterface\n{\n    // Extracting pagination information from the request, page = 1, size = 10 if it is missing\n    $pagination = $jsonApi-\u003egetPaginationFactory()-\u003ecreatePageBasedPagination(1, 10);\n\n    // Fetching a paginated collection of user domain objects\n    $users = UserRepository::getUsers($pagination-\u003egetPage(), $pagination-\u003egetSize());\n\n    // Instantiating a users document\n    $document = new UsersDocument(new UserResource(new ContactResource()));\n\n    // Responding with \"200 Ok\" status code along with the users document\n    return $jsonApi-\u003erespond()-\u003eok($document, $users);\n}\n```\n\n### Fetching a relationship\n\n```php\npublic function getBookRelationships(JsonApi $jsonApi): ResponseInterface\n{\n    // Getting the \"id\" of the currently requested book\n    $id = $jsonApi-\u003egetRequest()-\u003egetAttribute(\"id\");\n\n    // Getting the currently requested relationship's name\n    $relationshipName = $jsonApi-\u003egetRequest()-\u003egetAttribute(\"rel\");\n\n    // Retrieving a book domain object with an ID of $id\n    $book = BookRepository::getBook($id);\n\n    // Instantiating a book document\n    $document = new BookDocument(\n        new BookResource(\n            new AuthorResource(),\n            new PublisherResource(\n                new RepresentativeResource()\n            )\n        )\n    );\n\n    // Responding with \"200 Ok\" status code along with the requested relationship document\n    return $jsonApi-\u003erespond()-\u003eokWithRelationship($relationshipName, $document, $book);\n}\n```\n\n### Creating a new resource\n\n```php\npublic function createBook(JsonApi $jsonApi): ResponseInterface\n{\n    // Hydrating a new book domain object from the request\n    $book = $jsonApi-\u003ehydrate(new BookHydrator(), []);\n\n    // Saving the newly created book\n    // ...\n\n    // Creating the book document to be sent as the response\n    $document = new BookDocument(\n        new BookResource(\n            new AuthorResource(),\n            new PublisherResource(\n                new RepresentativeResource()\n            )\n        )\n    );\n\n    // Responding with \"201 Created\" status code along with the book document\n    return $jsonApi-\u003erespond()-\u003ecreated($document, $book);\n}\n```\n\n### Updating a resource\n\n```php\npublic function updateBook(JsonApi $jsonApi): ResponseInterface\n{\n    // Retrieving a book domain object with an ID of $id\n    $id = $jsonApi-\u003egetRequest()-\u003egetResourceId();\n    $book = BookRepository::getBook($id);\n\n    // Hydrating the retrieved book domain object from the request\n    $book = $jsonApi-\u003ehydrate(new BookHydrator(), $book);\n\n    // Updating the book\n    // ...\n\n    // Instantiating the book document\n    $document = new BookDocument(\n        new BookResource(\n            new AuthorResource(),\n            new PublisherResource(\n                new RepresentativeResource()\n            )\n        )\n    );\n\n    // Responding with \"200 Ok\" status code along with the book document\n    return $jsonApi-\u003erespond()-\u003eok($document, $book);\n}\n```\n\n### Updating a relationship of a resource\n\n```php\npublic function updateBookRelationship(JsonApi $jsonApi): ResponseInterface\n{\n    // Checking the name of the currently requested relationship\n    $relationshipName = $jsonApi-\u003egetRequest()-\u003egetAttribute(\"rel\");\n\n    // Retrieving a book domain object with an ID of $id\n    $id = $jsonApi-\u003egetRequest()-\u003egetAttribute(\"id\");\n    $book = BookRepository::getBook($id);\n    if ($book === null) {\n        die(\"A book with an ID of '$id' can't be found!\");\n    }\n\n    // Hydrating the retrieved book domain object from the request\n    $book = $jsonApi-\u003ehydrateRelationship($relationshipName, new BookHydrator(), $book);\n\n    // Instantiating a book document\n    $document = new BookDocument(\n        new BookResource(\n            new AuthorResource(),\n            new PublisherResource(\n                new RepresentativeResource()\n            )\n        )\n    );\n\n    // Responding with \"200 Ok\" status code along with the book document\n    return $jsonApi-\u003erespond()-\u003eok($document, $book);\n}\n```\n\n### How to try it out\n\nIf you want to see how Yin works, have a look at the [examples](https://github.com/woohoolabs/yin/tree/master/examples).\nIf `docker-compose` and `make` is available on your system, then just run the following commands in order to try out the\nexample API:\n\n```bash\ncp .env.dist .env      # You can now edit the settings in the .env file\nmake composer-install  # Install the Composer dependencies\nmake up                # Start the webserver\n```\n\nAnd finally, just visit the following URL: `localhost:8080`. You can even restrict the retrieved fields and relationships\nvia the `fields` and `include` parameters as specified by JSON:API.\n\nExample URIs for the book examples:\n- `GET /books/1`: Fetch a book\n- `GET /books/1/relationships/authors`: Fetch the authors relationship\n- `GET /books/1/relationships/publisher`: Fetch the publisher relationship\n- `GET /books/1/authors`: Fetch the authors of a book\n- `POST /books`: Create a new book\n- `PATCH /books/1`: Update a book\n- `PATCH /books/1/relationships/author`: Update the authors of the book\n- `PATCH /books/1/relationships/publisher`: Update the publisher of the book\n\nExample URIs for the user examples:\n- `GET /users`: Fetch users\n- `GET /users/1`: Fetch a user\n- `GET /users/1/relationships/contacts`: Fetch the contacts relationship\n\nWhen you finished your work, simply stop the webserver:\n\n```bash\nmake down\n```\n\nIf the prerequisites are not available for you, you have to set up a webserver, and install PHP on your host system as\nwell as the dependencies via `Composer`.\n\n## Integrations\n\n- [dimvic/yii-yin](https://github.com/dimvic/yii-yin): Integration for Yii 1.1\n- [paknahad/jsonapi-bundle](https://github.com/paknahad/jsonapi-bundle): Integration for Symfony\n- [qpautrat/woohoolabs-yin-bundle](https://github.com/qpautrat/woohoolabs-yin-bundle): Integration for Symfony\n\n## Versioning\n\nThis library follows [SemVer v2.0.0](https://semver.org/).\n\n## Change Log\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on recent changes.\n\n## Testing\n\nWoohoo Labs. Yin has a PHPUnit test suite. To run the tests, run the following command from the project folder:\n\n``` bash\n$ phpunit\n```\n\nAdditionally, you may run `docker-compose up` or `make test` in order to execute the tests.\n\n## Contributing\n\nPlease see [CONTRIBUTING](CONTRIBUTING.md) for details.\n\n## Support\n\nPlease see [SUPPORT](SUPPORT.md) for details.\n\n## Credits\n\n- [Máté Kocsis][link-author]\n- [All Contributors][link-contributors]\n\n## License\n\nThe MIT License (MIT). Please see the [License File](LICENSE) for more information.\n\n[ico-version]: https://img.shields.io/packagist/v/woohoolabs/yin.svg\n[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg\n[ico-build]: https://img.shields.io/github/workflow/status/woohoolabs/yin/Continuous%20Integration\n[ico-coverage]: https://img.shields.io/codecov/c/github/woohoolabs/yin\n[ico-code-quality]: https://img.shields.io/scrutinizer/g/woohoolabs/yin.svg\n[ico-downloads]: https://img.shields.io/packagist/dt/woohoolabs/yin.svg\n[ico-support]: https://badges.gitter.im/woohoolabs/yin.svg\n\n[link-version]: https://packagist.org/packages/woohoolabs/yin\n[link-build]: https://github.com/woohoolabs/yin/actions\n[link-coverage]: https://codecov.io/gh/woohoolabs/yin\n[link-code-quality]: https://scrutinizer-ci.com/g/woohoolabs/yin\n[link-support]: https://gitter.im/woohoolabs/yin?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\n[link-downloads]: https://packagist.org/packages/woohoolabs/yin\n[link-author]: https://github.com/kocsismate\n[link-contributors]: ../../contributors\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwoohoolabs%2Fyin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwoohoolabs%2Fyin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwoohoolabs%2Fyin/lists"}