kherge/resource-bundle

Makes resource API development a little easier.

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Watchers: 1

Forks: 0

Open Issues: 3

Type:symfony-bundle


README

Build Status Packagist Packagist Pre Release

KHerGe Resource Bundle

This project is a Symfony 3.0 bundle for creating resource-based RESTful APIs written for PHP 7.1. The bundle, however, does require you to conform to certain practices imposed by the bundle in order take advantage of its more advanced functionality.

Requirements

This bundle requires:

  • PHP 7.1+
  • Symfony 3.0+
  • FOSRestBundle 2.0+

Installation

  1. Install using Composer.

    composer require kherge/resource-bundle
    
  2. Add to your application kernel.

    $bundles[] = new KHerGe\Bundle\ResourceBundle\KHerGeResourceBundle();
    

Usage

This usage guide attempts to explain how the bundle is used beginning at the most basic level and then moving on to more advanced topics. Each subsequent topic builds on the knowledge gained from the previous one.

Resource

Creating a Resource

A resource is simply a class with scalar attributes and, optionally, associations to other resources. If you are familiar with Doctrine ORM's entity concept, you can consider a resource identical to a Doctrine ORM entity. The following example resource will manage the information of individual people.

namespace My\ExampleBundle\Resource;

use KHerGe\Bundle\ResourceBundle\Resource\ResourceInterface;
use KHerGe\Bundle\ResourceBundle\Resource\Traits\IdTrait;

/**
 * Manages the information for a person.
 */
class Person implements ResourceInterface
{
    use IdTrait;

    /**
     * The given name of the person.
     *
     * @var string
     */
    private $givenName;

    /**
     * The surname of the person.
     *
     * @var null|string
     */
    private $surname;

    /**
     * Initializes the new `Person` instance.
     *
     * @param string $givenName The given name of the person.
     */
    public function __construct(string $givenName)
    {
        $this->givenName = $givenName;
    }

    /**
     * Returns the given name of the person.
     *
     * @return string The given name.
     */
    public function getGivenName() : string
    {
        return $this->givenName;
    }

    /**
     * Returns the surname of the person.
     *
     * @return null|string The surname.
     */
    public function getSurname() : ?string
    {
        return $this->surname;
    }

    /**
     * Sets the given name of the person.
     *
     * @param string $givenName The given name.
     */
    public function setGivenName(string $givenName) : void
    {
        $this->givenName = $givenName;
    }

    /**
     * Sets the surname of the person.
     *
     * @param null|string $surname The surname.
     */
    public function setSurname(?string $surname) : void
    {
        $this->surname = $surname;
    }
}

As you can tell from this line,

class Person implements ResourceInterface

All resources must implement the ResourceInterface interface that is supplied by the bundle. This interface ensures that all resources can be uniquely identified for many of the create, read, update, and delete operations that the resource will be a part of.

This line,

use IdTrait;

takes care of implementing all of the identity requirements set by the resource interface. While using the trait is not required, it is the easiest approach. You may, however, opt to use your own way of setting and retrieving unique identifiers.

With our resource class completed, we will now need to create a form type that will be used for creating and modifying the values for this type of resource.

Creating a Resource Form Type

For this part, you will need a basic understanding of form types with the Symfony Form component. You can learn about creating custom form types at the documentation linked.

In order process requests for creating and updating Person resources, the bundle requires that a form type be created for that specific resource. The complexity of the form type will depend on how much control you want over what consumers of your API is allowed to provide.

For our Person form type, we will want to enforce a few constraints.

namespace My\ExampleBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

/**
 * A form type for the `Person` resource.
 */
class PersonType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Add a form field for the `givenName` attribute.
        $builder->add(
            'givenName',
            TextType::class,
            [
                'constraints' => [

                    // Limit the given name to 50 characters.
                    new Length(['max' => 50]),

                    // The given name is required.
                    new NotBlank()

                ]
            ]
        );

        // Add a form field for the `surname` attribute.
        $builder->add(
            'surname',
            TextType::class,
            [
                'constraints' => [

                    // Limit the surname to 50 characters.
                    new Length(['max' => 50])

                ]
            ]
        );
    }
}

For the most part, there should be no surprises. Because this example is aiming to use much of the bundle's built-in functionality, the names of the form fields must match the names of the resource attributes. More information on this will be provided later.

With our form type completed, we now need a way to store and retrieve our resources.

Creating a Resource Repository

In order to store and retrieve our Person resources, we need a repository specifically for that type of resource. The following is the most basic implementation of a repository:

namespace My\ExampleBundle\Repository;

use KHerGe\Bundle\ResourceBundle\Exception\InvalidArgumentException;
use KHerGe\Bundle\ResourceBundle\Exception\Resource\NotExistException;
use KHerGe\Bundle\ResourceBundle\Exception\Resource\NotUniqueException;
use KHerGe\Bundle\ResourceBundle\Resource\Repository\RepositoryInterface;
use KHerGe\Bundle\ResourceBundle\Resource\ResourceInterface;

/**
 * A repository for `Person` resources.
 */
class PersonRepository implements RepositoryInterface
{
    /**
     * The `Person` resources.
     *
     * @var ResourceInterface[]
     */
    private $resources = [];

    /**
     * Loads the resources from a file.
     *
     * The constructor is not part of the `RepositoryInterface` interface. It
     * is used so that we can have a simple persistent store of resources
     * that can be accessed across requests.
     */
    public function __construct()
    {
        $this->resources = unserialize(file_get_contents('people'));
    }

    /**
     * Saves the resource to a file.
     *
     * The destructor is not part of the `RepositoryInterface` interface. It
     * is used so that we can save changes made to the repository that will
     * later be loaded in a new process.
     */
    public function __destruct()
    {
        file_put_contents(serialize($this->resources));
    }

    /**
     * {@inheritdoc}
     */
    public function add(ResourceInterface $resource) : void
    {
        if (null === $resource->getId()) {
            $resource->setId(spl_object_hash($resource));
        }

        if (isset($this->resources[$resource->getId()])) {
            throw new NotUniqueException(
                'The resource is not unique.'
            );
        }

        $this->resources[$resource->getId()] = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function get($id) : ?ResourceInterface
    {
        if (!isset($this->resources[$id])) {
            return null;
        }

        return $this->resources[$id];
    }

    /**
     * {@inheritdoc}
     */
    public function has(ResourceInterface $resource) : bool
    {
        if (null === $resource->getId()) {
            return false;
        }

        return isset($this->resources[$resource->getId()]);
    }

    /**
     * {@inheritdoc}
     */
    public function remove(ResourceInterface $resource) : void
    {
        if (null === $resource->getId()) {
            throw new InvalidArgumentException(
                'The resource does not have a unique identifier set.'
            );
        }

        if (!isset($this->resources[$resource->getId()])) {
            throw new NotExistException(
                'The resource "%s" does not exist in this repository.',
                $resource->getId()
            );
        }

        unset($this->resources[$resource->getId()]);
    }

    /**
     * {@inheritdoc}
     */
    public function update(
        ResourceInterface $original,
        ResourceInterface $updated
    ) : void {
        if (!isset($this->resources[$original->getId()])) {
            throw new NotExistException(
                'The resource "%s" does not exist in this repository.',
                $original->getId()
            );
        }

        $this->resources[$original->getId()] = $updated;
    }
}

As you have noticed, the repository class implements a bundle interface in this line:

class PersonRepository implements RepositoryInterface

The KHerGe\Bundle\ResourceBundle\Resource\Repository\RepositoryInterface interface, requires that the following methods be defined in the repository:

interface RepositoryInterface
{
    public function add(ResourceInterface $resource) : void;
    public function get($id) : ?ResourceInterface;
    public function has(ResourceInterface $resource) : bool;
    public function remove(ResourceInterface $resource) : void;
    public function update(
        ResourceInterface $original,
        ResourceInterface $updated
    ) : void;
}

This basic repository implementation will allow us to store and retrieve Person resources using a PHP serialized file. For base resource management, this may be sufficient. For more advanced repository functionality, the bundle includes additional interfaces:

  • KHerGe\Bundle\ResourceBundle\Resource\Repository\IterableInterface - A repository that implements this interface allows for the availability of an API that will let clients iterate through all available resources.
  • KHerGe\Bundle\ResourceBundle\Resource\Repository\SearchableInterface - A repository that implements this interface allows for the availability of an API that will let clients search through all available resources.

In reality, you would probably want to use a database server to store and retrieve resources. In those cases, you will want to create your own repository implementation that stores and retrieves resources in the database.

Register the Resource Repository as a Service

Now that we have our repository, we need to make it available as a service. If you are not familiar with service definitions, please refer to the Symfony documentation. For convenience, you could probably use something like the following in services.yml:

services:
    my_example.resource.person_repository:
        class: My\ExampleBundle\Repository\PersonRepository

Registering the Resource

With our resource class, form type, and repository all done we can now bring all of these pieces together into a resource management API. To make this happen, we need to add a few settings to the bundle.

# KHerGeBundleResource
kherge_resource:

    # Register our resources.
    resources:

        # Add our `Person` resource.
        - class: ExampleBundle:Person
          form_type: ExampleBundle:PersonType
          repository: my_example.resource.person_repository

With this step completed, the service container now has the following services available:

  • my_example.resource.person_controller
  • my_example.resource.person_form_factory
  • my_example.resource.person_form_manager
  • my_example.resource.person_manager
  • my_example.resource.person_repository
  • my_example.resource.person_transformer

The service of interest is my_example.resource.person_controller as it allows us to configure the routes using FOSRestBundle. In your routing.yml configuration file:

# ExampleBundle -> Person
my_example_person:

    # Prefix generated route names.
    name_prefix: my_example_person_

    # Prefix generated route paths.
    prefix: /api/resources/persons

    # Use the resource controller registered by the bundle.
    resource: my_example.resource.person_controller

    # The KHerGeResourceBundle should process this.
    type: resource

With this final piece of the puzzle done, you should now have the following resource management APIs available for the Person resource:

  • GET /api/resources/persons/search.{_format}
  • GET /api/resources/persons.{_format}
  • DELETE /api/resources/persons/{id}.{_format}
  • GET /api/resources/persons/{id}.{_format}
  • PATCH /api/resources/persons/{id}.{_format}
  • POST /api/resources/persons.{_format}
  • PUT /api/resources/persons/{id}.{_format}

Using the Resource API

Now that we have our resource APIs, how do we use them?

For any of the APIs below, if the user does not have the required privileges to access the API a 403 response is returned.

Searching for Resources

Requires Privilege: ResourceVoter::READ

If the resource has a repository that implements KHerGe\Bundle\ResourceBundle\Resource\Repository\SearchableInterface and a form factory that implements KHerGe\Bundle\ResourceBundle\Resource\Form\Factory\SearchableInterface, you will have access to an API that will allow you to search the repository for relevant resources.

GET /api/resources/persons?query=givenName:kevin&offset=2&limit=10

The search API accepts the following parameters:

  • query - This is the query string that is passed on to the repository. It is the responsibility of the repository to process the search query.
  • offset - The offset is used to paginate through a list of resources. If the limit is 10 and the offset is 2, the return list of resources will skip the first 10 resources and display the next 10.
  • limit - The limit determines how may results are returned at a time. The maximum limit is determined by the bundle setting kherge_resource.search.limit.

In our hypothetical searchable repository, the givenName:kevin search query would find all resources that have the given name kevin (case insensitive). The response would look something like the following (depends on FOSRestBundle configuration):

[
    {
        "id": 123,
        "givenName": "Kevin",
        "surname": "Herrera"
    },
    {
        "id": 456,
        "givenName": "Kevin",
        "surname": "Sorbo"
    },
    {
        "id": 789,
        "givenName": "Kevin",
        "surname": "Spacey"
    }
]
Iterating the Repository

Requires Privilege: ResourceVoter::READ

If the resource has a repository that implements KHerGe\Bundle\ResourceBundle\Resource\Repository\IterableInterface and a form factory that implements KHerGe\Bundle\ResourceBundle\Resource\Form\Factory\IterableInterface, you will have access to an API that will allow you to iterate the contents of the repository.

GET /api/resources/persons?offset=2&limit=10&sort=surname:asc

The iteration API accepts the following parameters:

  • offset - The offset is used to paginate through a list of resources. If the limit is 10 and the offset is 2, the return list of resources will skip the first 10 resources and display the next 10.
  • limit - The limit determines how may results are returned at a time. The maximum limit is determined by the bundle setting kherge_resource.paginate.limit.
  • sort - The sort field uses a formatted value to determine what fields are sorted in what order. If we want to sort by surname and then given name, we would use something like surname:asc,givenName:asc.

In our hypothetical iterable repository, the example request above would return 10 resources on page 2 sorted by their surname in ascending order. The response would look something like the following (depends on FOSRestBundle configuration):

[
    {
        "id": 1962,
        "givenName": "Spider",
        "surname": "Man"
    },
    {
        "id": 196339,
        "givenName": "Tony",
        "surname": "Stark"
    },
    {
        "id": 1963,
        "givenName": "Charles",
        "surname": "Xavier"
    }
]
Deleting a Resource

Requires Privilege: ResourceVoter::DELETE

DELETE /api/resources/persons/123

This request will delete an existing resource and return a 201 (no content) response. If the resource does not exist, a 404 response will be returned.

Retrieving a Resource

Requires Privilege: ResourceVoter::READ

GET /api/resources/persons/123

This request will retrieve an existing resource and return a 200 (ok) response. The response would like something like the following (depends on FOSRestBundle configuration):

{
    "id": 123,
    "givenName": "Kevin",
    "surname": "Herrera"
}

If the resource does not exist a 404 response is returned.

Updating a Resource

Requires Privilege: ResourceVoter::UPDATE

PATCH /api/resources/persons/123
givenName=David

This request will update an existing resource. Only the parameters provided will be set for the resource. The 200 (ok) response would like something like the following (depends on FOSRestBundle configuration):

{
    "id": 123,
    "givenName": "David",
    "surname": "Herrera"
}

If the resource does not exist a 404 response is returned.

Creating a Resource

Requires Privilege: ResourceVoter::CREATE

POST /api/resources/persons
givenName=James&surname=Bond

This request creates a new resource. If a new resource is successfully created, a new unique identifier is generated by the repository for the resource. The 201 (created) response would look something like the following (depends on FOSRestBundle configuration):

{
    "id": 7,
    "givenName": "James",
    "surname": "Bond"
}

If the resource could not be created, a 400 response is returned and the serialized representation of the form is returned as the body.

Creating a New or Replacing an Existing Resource

Requires Privilege: ResourceVoter::CREATE or ResourceVoter::UPDATE

PUT /api/resources/persons/123
givenName=Mister&surname=X

This request will either create a new resource or replace an existing resource with a unique identifier of 123. If the a new resource is created, a 201 (created) response is returned. If an existing resource is replaced, a 200 (ok) response is returned. The response body would look something like the following (depends on FOSRestBundle configuration):

{
    "id": 123,
    "givenName": "Mister",
    "surname": "X"
}

Associations

This part of the usage guide covers how associations between different resources can also be managed to the resource management API. If your resources do not refer to each other, it is safe to skip this section.

Creating an Association

For the sake of simplicity, we are going to re-use the Person resource to establish an association between parent and child. To start, we need to modify our existing Person resource to manage this association.

namespace My\ExampleBundle\Resource;

use KHerGe\Bundle\ResourceBundle\Resource\ResourceInterface;
use KHerGe\Bundle\ResourceBundle\Resource\Traits\IdTrait;

/**
 * Manages the information for a person.
 */
class Person implements ResourceInterface
{
    // ... snip ...

    /**
     * The children of this person.
     *
     * @var Person[]
     */
    private $children = [];

    /**
     * Adds a child to the person.
     *
     * @param Person $child The child to add.
     */
    public function addChild(Person $child) : void
    {
        $this->children[] = $child;
    }

    /**
     * Returns the children for the person.
     *
     * @return Person[] The children.
     */
    public function getChildren() : array
    {
        return $this->children;
    }

    /**
     * Checks if a person is a child of this person.
     *
     * @param Person $child The child to check.
     *
     * @return boolean Returns `true` if it is a child or `false` if not.
     */
    public function hasChild(Person $child) : bool
    {
        return in_array($child, $this->children, true);
    }

    /**
     * Removes a person as a child from this person.
     *
     * @param Person $child The child to remove.
     */
    public function removeChild(Person $child) : void
    {
        $keys = array_keys($this->children, $child, true);

        foreach ($keys as $key) {
            unset($this->children[$key]);
        }
    }

    /**
     * Replaces all current children with new children for the person.
     *
     * @param Person[] $children The children to replace with.
     */
    public function setChildren(array $children) : array
    {
        $this->children = $children;
    }

    // ... snip ...
}

Our changes will allow us to add, check, remove, and replace children for any person. This allows us to move on to the next step of creating an association resource controller.

Some keen-eyed readers may notice that, because of the way the repository stores and loads resources, the children will become detached from the repository on the subsequent load. For the examples here, assume that this does not happen and that any child associated to the person is also in the repository.

Creating an Association Resource Controller

In order to expose the ability to manage the children of the person resource, we need to create an association resource controller.

The Easy Way

You can use the bundled KHerGe\Bundle\ResourceBundle\Resource\Controller\AssociationController by simply registering the known associations with the bundle.

kherge_resource:

    # Register our associations.
    associates:

        # Register the associations for our `Person` resource.
        My\ExampleBundle\Resource\Person:

            # Map the `children` attribute to the `Person` resource.
            children: My\ExampleBundle\Resource\Person

            # ... we could list more here if we hand any ...

With that bundle configuration completed, the following service is now available:

my_example.resource.person_associates_controller
The Hard Way

You can create an association controller yourself by implementing KHerGe\Bundle\ResourceBundle\Resource\Controller\Association\AssociationControllerInterface, or you can take the easier route by extending KHerGe\Bundle\ResourceBundle\Resource\Controller\Association\AbstractAssociationController.

It is important to understand that the AbstractAssociationController controller is designed to support any association where the resource is on the owning side.

The example association resource controller below only demonstrates support for the "children" association, but you can just add to the if/else branches to add support for additional associations.

namespace My\ExampleBundle\Controller;

use KHerGe\Bundle\ResourceBundle\Resource\Controller\Association\AbstractAssociationController;
use KHerGe\Bundle\ResourceBundle\Resource\Repository\ResourceIterator;
use My\ExampleBundle\Resource\Person;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Manages associations for the `Person` resource.
 */
class PersonAssociationsController extends AbstractAssociationController
{
    /**
     * {@inheritdoc}
     */
    protected function addAssociate(
        ResourceInterface $resource,
        string $attribute,
        ResourceInterface $associate
    ) : void {
        if ('children' === $attribute) {
            $resource->addChild($associate);
        } else {
            throw new NotFoundHttpException(
                "A(n) \"$attribute\" association does not exist."
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function getAssociate(
        ResourceInterface $resource,
        string $attribute,
        string $id
    ) : ResourceInterface {
        if ('children' === $attribute) {
            $child = $this
                ->getRegistry()
                ->get(Person::class)
                ->getRepository()
                ->get($id)
            ;

            if (null === $child) {
                throw new NotFoundHttpException(
                    "A resource with \"$id\" for \"$attribute\" does not exist."
                );
            }

            return $child;
        } else {
            throw new NotFoundHttpException(
                "A(n) \"$attribute\" association does not exist."
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function getAssociates(
        ResourceInterface $resource,
        string $attribute,
        int $offset,
        int $limit,
        array $sort
    ) : ResourceIteratorInterface {
        if ('children' === $attribute) {
            return new ResourceIterator(
                array_slice(
                    $this->sort($resource->getChildren(), $sort),
                    $offset,
                    $limit
                )
            );
        } else {
            throw new NotFoundHttpException(
                "A(n) \"$attribute\" association does not exist."
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function removeAssociate(
        ResourceInterface $resource,
        string $attribute,
        ResourceInterface $associate
    ) : void {
        if ('children' === $attribute) {
            if (!$resource->hasChild($associate)) {
                throw new NotFoundHttpException(
                    "The \"$attribute\" resource is not associated."
                );
            }

            $resource->removeChild($associate);
        } else {
            throw new NotFoundHttpException(
                "A(n) \"$attribute\" association does not exist."
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function replaceAssociates(
        ResourceInterface $resource,
        string $attribute,
        array $ids
    ) : void {
        if ('children' === $attribute) {
            $children = [];
            $missing = [];
            $repository = $this
                ->getRegistry()
                ->get(Person::class)
                ->getRepository()
            ;

            foreach ($ids as $id) {
                $child = $repository->get($id);

                if (null === $child) {
                    $missing[] = $id;
                } else {
                    $children[] = $child;
                }
            }

            if (!empty($missing)) {
                throw new NotFoundHttpException(
                    sprintf(
                        'The following "%s" resources were not found: %s',
                        $attribute,
                        join(', ', $missing)
                    )
                );
            }

            $resource->setChildren($children);
        } else {
            throw new NotFoundHttpException(
                "A(n) \"$attribute\" association does not exist."
            );
        }
    }

    /**
     * My magical sorting function.
     *
     * The contents of the sort array could look something like this:
     *
     *     [
     *         'surname' => 'DESC',
     *         'givenName' => 'ASC'
     *     ]
     *
     * While the values will always be either `ASC` or `DESC`, you will need
     * to determine for yourself whether or not the keys are valid. It would
     * be acceptable to simply ignore keys for fields that do not exist.
     *
     * @param array $resource The resources to sort.
     * @param array $sort     The fields to sort and their orders.
     *
     * @return array The sorted resources.
     */
    private function sort(array $resources, array $sort) : array
    {
        /*
         * It will be up to you to implement this part. In cases where
         * resources come from a database, a database query would be able
         * to handle this for you.
         */
    }
}

Now that we have our association resource controller, we need to register it as a service. This is necessary as the controller relies on several services in order to function correctly. To simplify this step, a kherge_resource.association_controller abstract service is defined for us to use.

services:
    my_example.resource.person_associates_controller:
        class: My\ExampleBundle\Controller\PersonAssociationsController
        parent: kherge_resource.association_controller

Registering the Association Resource Controller

With our association controller registered as a service, we need to register it with the router.

# ExampleBundle -> Person Associations
my_example_person_assocations:

    # Prefix generated route names.
    name_prefix: my_example_person_assocation_

    # Prefix generated route paths.
    prefix: /api/resources/persons

    # Use the association resource controller registered earlier.
    resource: my_example.controller.person_associations

    # KHerGeResourceBundle should process these settings.
    type: associate

With this last step complete, we now have a few additional routes:

  • GET /api/resources/persons/{resourceId}/{attribute}.{_format}
  • DELETE /api/resources/persons/{resourceId}/{attribute}/{associateId}.{_format}
  • POST /api/resources/persons/{resourceId}/{attribute}/{associateId}.{_format}
  • PUT /api/resources/persons/{resourceId}/{attribute}.{_format}

Using the Association API

For any of the APIs below, if the user does not have the required privileges to access the API a 403 response is returned.

In order to access any of the APIs below, the client will need read access to the resource being used as the pivot for any of the associations.

Retrieving Associations

Requires Privilege: ResourceVoter::READ

GET /api/resources/persons/123/children?offset=2&limit=10&sort=surname:asc

This action will allow you to iterate through all of the associated children resources for the parent resource. The iteration API accepts the following parameters:

  • offset - The offset is used to paginate through a list of resources. If the limit is 10 and the offset is 2, the return list of resources will skip the first 10 resources and display the next 10.
  • limit - The limit determines how may results are returned at a time. The maximum limit is determined by the bundle setting kherge_resource.paginate.limit.
  • sort - The sort field uses a formatted value to determine what fields are sorted in what order. If we want to sort by surname and then given name, we would use something like surname:asc,givenName:asc.

The example request above would return 10 resources on page 2 sorted by their surname in ascending order. The response would look something like the following (depends on FOSRestBundle configuration):

[
    {
        "id": 1962,
        "givenName": "Spider",
        "surname": "Man"
    },
    {
        "id": 196339,
        "givenName": "Tony",
        "surname": "Stark"
    },
    {
        "id": 1963,
        "givenName": "Charles",
        "surname": "Xavier"
    }
]
Deleting an Association

Requires Privilege: ResourceVoter::UPDATE

DELETE /api/resources/persons/123/children/456

This request will delete an existing association between resources and return a 201 (no content) response. If the resources never had an association established, a 404 response will be returned.

Creating an Association

Requires Privilege: ResourceVoter::UPDATE

POST /api/resources/persons/123/children/456

This request will create a new association between the two resources and return a 201 (no content) response. If the association (e.g. children) or resources do not exist a 404 (not found) response is returned.

Replacing All Associations

Requires Privilege: ResourceVoter::UPDATE

PUT /api/resources/persons/123/children
id[]=456&id[]789

This request will replace all of the current associations with the new ones provided. If no new resources to associate are specified, then all of the current associations will simply be removed. In either case, a 201 (no content) response is returned. If any of the resources do not exist, a 404 (not found) response is returned.

API Responses

How a response is rendered by the controller depends on two factors:

  1. How the FOSRestBundle has been configured.
  2. How the configured serializer normalizes data.

To understand how the configuration for the FOSRestBundle affects the rendering of the views used by the resource controller, you will need to refer to the FOSRestBundle documentation.

How a serializer normalizes the view data depends on which serializer is configured for the FOSRestBundle. The FOSRestBundle supports the Symfony Serializer and the JMS Serializer. Please refer to their respective documentation to learn more.

Security

Resource Voter

The default configuration registers a custom voter (KHerGe\Bundle\ResourceBundle\Security\Voter\ResourceVoter) that supports all resources for a specific set of attributes. These attributes are:

  • ResourceVoter::CREATE (resource.create)
  • ResourceVoter::DELETE (resource.delete)
  • ResourceVoter::READ (resource.read)
  • ResourceVoter::UPDATE (resource.update)

Using the Person resource as an example, you would check to if the currently authenticated user has the privilege to delete a Person resource by doing the following:

$authorizationChecker->isGranted(ResourceVoter::DELETE, $resource);

If a $resource instance is not available, you must instead pass the name of the resource class. This will work for all attributes.

Continuing with the example above, the voter will generate three role names and use the access decision manager to see if the currently authenticated user has any of those roles assigned.

  • ROLE_RESOURCE_ADMIN
  • ROLE_RESOURCE_PERSON_ADMIN
  • ROLE_RESOURCE_PERSON_DELETE

For privilege checks using the other attributes, the DELETE role may instead be one of the following:

  • ROLE_RESOURCE_PERSON_CREATE
  • ROLE_RESOURCE_PERSON_READ
  • ROLE_RESOURCE_PERSON_UPDATE

To help take the mystery out of what role names are used, a command is available to generate the role names that you could expect the resource voter to use depend on what resource class name is provided.

$ app/console resource:role-names 'My\ExampleBundle\Resource\Person'
Class: My\ExampleBundle\Resource\Person
Roles:
  - ROLE_RESOURCE_ADMIN
  - ROLE_RESOURCE_PERSON_ADMIN
  - ROLE_RESOURCE_PERSON_CREATE
  - ROLE_RESOURCE_PERSON_DELETE
  - ROLE_RESOURCE_PERSON_READ
  - ROLE_RESOURCE_PERSON_UPDATE

Doctrine Support

Creating and registering your resources can be tedious, but that is not why this bundle exists. The purpose of this bundle is to integrate with another data manager, such as Doctrine ORM.

Setup

By default, support for Doctrine ORM is automatically enabled if DoctrineBundle is registered with the application. To force enable this feature, you can set kherge_resource.doctrine.enabled to true (please refer to the configuration reference).

With Doctrine ORM support enabled, all of the required services for any entity resource you have created will be automatically registered. The only remaining step is to register the controller with the router, just like you would with any other resource.

# ExampleBundle -> Entity\User
my_example_users:

    # Prefix generated route names.
    name_prefix: my_example_users_

    # Prefix generated route paths.
    prefix: /api/resources/users

    # Use the resource controller registered by the bundle.
    resource: my_example.resource.user_controller

    # The KHerGeResourceBundle should process this.
    type: resource

Configuration Reference

# Default configuration for extension with alias: "kherge_resource"
kherge_resource:

    # The resource associates to create association controllers for.
    associates:

        # Prototype
        -

            # The name of the resource class on the owning side.
            class:                ~ # Required

            # The attribute -> associate resource class map.
            map:

                # Prototype
                -

                    # The attribute for the association.
                    attribute:            ~ # Required

                    # The associate resource class name.
                    class:                ~ # Required

    # The collection accessor settings.
    collection_accessor:

        # The cache service to use.
        cache:                ~

        # Throw an exception if a collection does not exist?
        exception:            true

        # Allow use of magic methods?
        # (e.g. __call, __get, __set)
        magic:                true

    # The Doctrine entity resource settings.
    doctrine:

        # Enable support for Doctrine entity resources?
        # (If `null`, automatically set to `true` if Doctrine is available.)
        enabled:              ~

        # The map of Doctrine field types to form types.
        # (Defaults are always registered but can be overridden.)
        types:

            # Defaults:
            array:               KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\ArrayType
            bigint:              KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\BigIntType
            blob:                KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\BlobType
            boolean:             KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\BooleanType
            date:                KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\DateType
            datetimez:           KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\DateTimeZType
            decimal:             KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\DecimalType
            float:               KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\FloatType
            guid:                KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\GuidType
            integer:             KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\IntegerType
            json_array:          KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\JsonArrayType
            object:              KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\ObjectType
            smallint:            KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\SmallIntType
            simple_array:        KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\SimpleArrayType
            string:              KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\StringType
            text:                KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\TextType
            time:                KHerGe\Bundle\ResourceBundle\Doctrine\Form\Type\DateTimeType

        # Enable support for validation constraints?
        # (If `null`, automatically set to `true` if validation is available.)
        validation:           ~

    # The resource paginator settings.
    paginate:

        # The maximum number of resource per page allowed.
        # (If the form input value exceeds this value, this value is used instead.)
        limit:                25

    # The resources to register.
    resources:

        # Prototype: The resource to register.
        -

            # The name of the resource class.
            # (e.g. TestBundle:Example or My\TestBundle\Resource\Example)
            class:                ~ # Required

            # The service identifier for the resource controller to use.
            # (If none is provided, one will be automatically registered.)
            controller:           ~

            # The service identifier for the resource form factory to use.
            # (If none is provided, one will be automatically registered.)
            form_factory:         ~

            # The service identifier for the resource form factory to use.
            # (If none is provided, one will be automatically registered.)
            form_manager:         ~

            # The name of the resource form type class.
            # (e.g. TestBundle:ExampleType or My\TestBundle\Form\Type\ExampleType)
            form_type:            ~ # Required

            # The prefix to use when registering resource services.
            id_prefix:            ~

            # The service identifier for the resource repository to use.
            repository:           ~ # Required

            # The role name template to use.
            role_template:        ~

    # The resource routing settings.
    routing:

        # Include the format placeholder in the paths?
        # (e.g. `/example/123` -> `/examples/123.json`)
        include_format:       true

        # Pluralize the generated resource paths?
        # (e.g. `/example/123` -> `/examples/123`)
        pluralize:            true

    # The resource search settings.
    search:

        # The maximum number of resource results per page allowed.
        # (If the form input value exceeds this value, this value is used instead.)
        limit:                25

    # The resource security settings.
    security:

        # The resource role name generator settings.
        role_generator:

            # The template to use for the resource role name template.
            template:             ROLE_RESOURCE_%%s_%%%%s

        # The resource voter settings.
        voter:

            # Register the bundled resource voter?
            enabled:              true

            # The name of the resource super admin role.
            # (If `null` is provided, the role is not used.)
            super_admin:          ROLE_RESOURCE_ADMIN

License

This project is dual-licensed under the MIT and Apache 2.0 licenses. Pick the license that best works for your project!