demos-europe / edt

Enables your PHP application to expose its entities as REST resources using the feature rich JSON:API specification as API. How and to whom your entities are exposed is highly customizable while minimizing boilerplate code.
MIT License
2 stars 1 forks source link
crud-api doctrine-orm json-api php querying restrict-access sorting

= EDT :toc: :sectanchors:

image:https://img.shields.io/packagist/v/demos-europe/edt-jsonapi.svg[Packagist Version] image:https://img.shields.io/packagist/php-v/demos-europe/edt-jsonapi.svg[PHP from Packagist] image:https://img.shields.io/librariesio/github/demos-europe/edt.svg[Dependencies] image:https://img.shields.io/badge/phpstan_level-9-green[Phastan-9] image:https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white["Conventional Commits",link=https://conventionalcommits.org]

This is the EDT monorepo, containing the development of multiple composer packages. In conjunction these packages can be used in PHP applications (if they already use Doctrine as ORM framework) to define a functional web-API contract, adhering to the https://jsonapi.org/format/1.0/[JSON:API specification].

== About EDT

This library enables you to expose the entity class instances in your PHP application as REST resources using the feature rich link:https://jsonapi.org/[JSON:API specification] as API contract. How and to whom you expose your entities is highly customizable while minimizing the need of manually written boilerplate code.

Without custom extensions, usage is currently limited to applications building their entities uppon the link:https://www.doctrine-project.org/projects/orm.html[Doctrine ORM]. However, as the different underlying functionalities are split and encapsulated in many individual PHP classes, put together via constructor-based dependency injection, a high degree of customizability and extensibility is ensured.

These classes are structured into multiple composer packages contained in this repository, to ease re-usage for different use cases without the need to include large, unnecessary dependencies. E.g. instead of using it as a whole, you may want to use parts of this library to build your own API implementation more easily. If you do, you can simply skip the Doctrine ORM dependency and implement your own way to access the data source of your entities.

== When to use

Due to the variety of features provided by the JSON:API specification, implementing it in a maintainable way may provide a challenge. After an initial setup, this library reduces your duty to expressing your intents regarding your web-API contract once, avoiding the risks of inconsistencies due to redundancy.

This is especially helpful in large, volatile, fast-growing web-applications with demanding clients, where most new business features may require new entities or properties to be exposed to them.

On the other hand, for small and relatively settled applications, this library may not be the right choice. This is due to the currently notable time cost for the initial setup, especially in case of applications that don't already use Symfony and the Doctrine ORM framework. At some point the setup difficulty may be reduced, but for now, batteries are not included.

== How to use

NOTE: The xref:initial-setup.adoc[initial setup] is quite complex and a considerable entry barrier. But as it only needs to be done once, this section will attempt to give an idea of the more relevant typical day-to-day usage instead.

When you express your intent regarding your API's capabilities and schema with this library, it is logically centered around the resources and their properties.

For example, if you wanted to expose your Book entity of your bookstore, you'd write a corresponding PHP configuration, defining which properties of the Book entity are to be available via Book resources. For the following example we assume that you also want to expose Person entities as Author resources, with each book having potentially multiple authors.

[mermaid] ifdef::env-github[[source,mermaid]] .... erDiagram Book { string id string title int price } Person { string id string fullName } Person ||--o{ Book : books Book ||--o{ Person : authors ....

The library allows for xref:implementing-types.adoc[multiple approaches] to define your resources. The following code shows an example how to use the highest level of abstraction provided by the library: type config classes. Using these you can create a configuration that will be applied to all resources of a specific type (i.e. using the same value in their type field).

After creating the configuration instance you have access to methods to set some general information and behavior of the type, but you can also configure the schema of the type by accessing the corresponding properties on the type config instance.

[source,php]

$bookConfig = new BookBasedResourceConfig($propertyFactory); <1> $authorConfig = new PersonBasedResourceConfig($propertyFactory); <2>

$bookConfig->id ->setReadableByPath(); $bookConfig->title <3> ->setReadableByPath() ->setSortable() ->setFilterable(); $bookConfig->price ->setReadableByPath() ->setFilterable() ->addPathUpdateBehavior(); <4> $bookConfig->authors ->setRelationshipType($authorConfig) <5> ->setFilterable() ->setReadableByPath() ->addPathUpdateBehavior();

$authorConfig->setTypeName('Author'); <6> $authorConfig->id ->setReadableByPath(); $authorConfig->fullName ->setReadableByPath() ->setFilterable(); $authorConfig->books ->setRelationshipType($bookConfig) <7> ->setReadableByPath();


<1> You can automatically generate and re-generate a basic configuration template class like `BookBasedResourceConfig` from your entity class (i.e. `Book`). This avoids manually writing and maintaining some boilerplate code and helps for its actual configuration, e.g. by providing proper type hinting and IDE autocompletion support. <2> Because resources often reference each other via relationship properties, we first create all configuration instances and configure them afterward. <3> Individual properties can be configured using link:https://en.wikipedia.org/wiki/Fluent_interface[fluent, self-referential] methods. I.e. each method call on a property will return that property, so it can be used for further method calls. <4> To keep this example small, the `price` attribute and `authors` relationship are the only properties that can be updated via the JSON:API. For the same reason we omitted other features here, like default sorting, limiting access to instances and the creation of resources, which are all possible too. <5> When configuration template classes are generated, the `authors` relationship from the `Book` entity to the `Person` entity was automatically deduced, but you may expose your `Person` entity via different resource types, using different configurations each. Thus, you need to define which of them the `Book` resources references, by setting the specific `$authorConfig` configuration. <6> By default, the type name of your exposed resources will be the same as the name of the backing entity. This is correct for the `Book` resources, but for the `Author` resources we need to specify the name, otherwise `Person` would be used as resource type name. <7> By not only referencing the authors by the `Book` resource but also the books by the `Author` resource, we created a bidirectional relationship, which is not necessary and (without workarounds) requires a corresponding relationship in the `Person` entity, but provides greater flexibility to the client. When a request is received, the library will automatically validate it against the configuration above and deny it, if access violations are detected. Access is only granted as defined, no other entities or properties will be exposed and the exposed properties can only be used as configured. NOTE: In this basic example we simply exposed all `Book` and `Person` entities that are stored in your data source as `Book` and `Author` resources and didn't distinguish access rights based on authorizations. In practice this may often not be acceptable. We also assumed that all exposed resource properties have a corresponding property in the entity, which is also not always the case. Consequently, the configuration classes provide additional methods to control the access and mitigate schema divergences. Those methods and others, e.g. to configure the creation of resources, were not shown in the example above. For a more thorough overview of the configuration capabilities as Q&A, please see xref:configuring_resources.adoc[Configuring Resources]. Even though the two resource configurations were mostly done independently, their definition as shown above directly result in some synergies and multiple JSON:API specification features provided to the client, briefly explained in the following sections. === Includes and relationships Clients can not only fetch `Book` resources in one request and `Author` resources in another, but also state in the fetch request of one or multiple `Book` resources that referenced `Author` resources shall be included in the response. Because of the bidirectional relationship between books and authors this also works the other way around. From a security perspective this is of no concern, as it does not expose any additional data, but simply allows to request and use it in a more convenient way. Also, setting the `authors` relationship as (for example) updatable will allow to change the list of author, but the values set must be resources corresponding to the exact resource type set via `setResourceType($authorConfig)`. Otherwise, the request would be denied. === Sorting and filtering Setting the `fullName` attribute as filterable allows the client to fetch `Author` resources filtered by that attribute. But by setting the `authors` relationship in the `Book` resource as filterable as well, the client can now filter `Book` resources by the name of the corresponding authors of each book. The same is true in regard to sorting `Book` resources by the data stored in to-one relationships, as long as the corresponding properties are set as filterable. However, sorting by to-many relationships is currently not supported. I.e. you could sort `Book` resources by the name of their authors, if every book only has a single author. But you can't do that if a single book has many authors. While the JSON:API specification provides a format to define the intended sorting, it leaves the format to define filters open. By default, this library supports the standard link:https://www.drupal.org/docs/8/modules/jsonapi/filtering[Drupal filter] format, with its shorthands not yet being supported. The Drupal filter format allows to define simple but also complex filters, including nested `AND`/`OR` groups and a set of many different operators. The implementation in this library is able to automatically convert such filters into the corresponding DQL query to fetch Doctrine entities. If wanted, support for custom operators can be added. Otherwise, no additional implementation is needed beside configuring resource properties as filterable as shown above. == Stability and development path Even though the packages are already used in production, they're not recommended for general usage yet. While development has settled down in some parts, in others refactorings are still necessary, resulting in deprecations and backward compatibility breaking changes. Because the initial setup still requires extensive work and crucial documentation is missing, the entry barrier can also be deemed too high. However, contributions are welcome: * https://github.com/demos-europe/edt/issues/new[Create a new issue (e.g. a question, bug report or feature request)] * https://github.com/demos-europe/edt/contribute[Work on open issues] * https://github.com/demos-europe/edt/pulls?q=is%3Aopen+is%3Apr+milestone%3A0.27.0+label%3APrerequisite[Review prerequisite pull requests] (*current bottleneck*) The objective is to get the project to a more stable state over the course of the year 2024, ideally releasing a xref:releasing.adoc#_stable_release[1.0.0 version with a more reliable API and proper documentation] before 2025. Even after a stable release, adding optional features and support for future JSON:API specification versions is left as an ongoing process. Similarly, easing the usage with applications not based on Symfony and Doctrine is not the scope of a first stable version. == Credits and acknowledgements Conception and implementation by Christian Dressler with many thanks to link:https://github.com/eFrane[eFrane]. Additional thank goes to link:https://demos-deutschland.de/[DEMOS plan GmbH], which provided the initial use-case and the opportunity to implement relevant parts to solve it.