Skip to content

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.

License

Notifications You must be signed in to change notification settings

demos-europe/edt

Repository files navigation

EDT

Packagist Version PHP from Packagist Dependencies Phastan-9 Conventional Commits

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 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 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 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 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.

erDiagram
    Book {
        string id
        string title
        int price
    }
    Person {
        string id
        string fullName
    }
    Person ||--o{ Book : books
    Book ||--o{ Person : authors
Loading

The library allows for 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.

$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 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 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 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.

The objective is to get the project to a more stable state over the course of the year 2024, ideally releasing a 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 eFrane.

Additional thank goes to DEMOS plan GmbH, which provided the initial use-case and the opportunity to implement relevant parts to solve it.

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages