programmingarehard / resource-bundle
Symfony bundle that helps in developing REST APIs.
Installs: 29
Dependents: 0
Suggesters: 0
Security: 0
Stars: 34
Watchers: 10
Forks: 2
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=5.3.2
- doctrine/inflector: ~1.0
- friendsofsymfony/rest-bundle: ~1.3
- symfony/form: ~2.1
- symfony/framework-bundle: ~2.1
- symfony/security-bundle: ~2.1
Requires (Dev)
- doctrine/orm: ~2.3
- phpspec/phpspec: 2.1.*@dev
Suggests
- jms/serializer-bundle: Add support for advanced serialization capabilities, recommended, requires 0.12.*
This package is auto-updated.
Last update: 2024-10-29 05:10:29 UTC
README
The ResourceBundle is an opinionated Symfony bundle to aid in developing REST APIs. It makes some architectural decisions for you, allowing you to focus more on the domain of your application. It uses as little magic as possible to make it easier to understand, debug, and extend.
##Prerequisites The ResourceBundle relies on the FOSRestBundle to handle content negotiation and RESTful decoding of request bodies. After installing the bundle, you must configure it before proceeding to use the ResourceBundle. Here is a sample configuration to get started.
Note: The ResourceBundle does not handle any sort of authentication. It is meant to be used in conjunction with something like the FOSOAuthServerBundle.
##Bundle Usage
- Resources
- Resource Repositories
- Resource Managers
- Resource Forms
- Form Handlers
- Form Processors
- Resource Controllers
- Resource Routing
- Events
- Bundle Configuration Reference
- Special Notes
##Resources The ResourceBundle is centered around resources. The bundle requires resources(entities) to implement the very simple ResourceInterface. The examples assume you're using Doctrine but the bundle is ORM agnostic. First, let's create a simple resource.
<?php // src/MyApp/CoreBundle/Entity/Task.php namespace MyApp\CoreBundle\Entity; use MyApp\CoreBundle\Domain\Task\TaskInterface; use ProgrammingAreHard\ResourceBundle\Domain\ResourceInterface; class Task implements TaskInterface, ResourceInterface { protected $id; protected $task; public function getId() { return $this->id; } public function isNew() { return null === $this->getId(); } public function getTask() { return $this->task; } public function setTask($task) { $this->task = $task; } }
##Resource Repositories Once we have a resource, it's time to create a repository for the resource by implementing the ResourceRepositoryInterface. If using Doctrine, just extend the bundled BaseResourceRepository.
<?php // src/MyApp/CoreBundle/Domain/Task/Repository/Doctrine/TaskRepository.php namespace MyApp\CoreBundle\Domain\Task\Repository\Doctrine; use MyApp\CoreBundle\Domain\Task\Repository\TaskRepositoryInterface; use MyApp\CoreBundle\Entity\Task; use ProgrammingAreHard\ResourceBundle\Domain\Repository\Doctrine\BaseResourceRepository; class TaskRepository extends BaseResourceRepository implements TaskRepositoryInterface { /** * {@inheritdoc} */ public function newInstance() { return new Task; } }
Register it with the container.
# src/MyApp/CoreBundle/Resources/services.yml services: myapp.task.repository: class: Doctrine\ORM\EntityRepository factory_service: doctrine.orm.entity_manager factory_method: getRepository arguments: - MyApp\CoreBundle\Entity\Task
Remember to update the mapping file.
# src/MyApp/CoreBundle/Resources/doctrine/Task.orm.yml MyApp\CoreBundle\Entity\Task: type: entity table: tasks repositoryClass: MyApp\CoreBundle\Domain\Task\Repository\Doctrine\TaskRepository id: id: type: integer generator: strategy: AUTO fields: task: type: string length: 255
##Resource Managers
Just like Doctrine, persisting and deleting resources is not done by repositories. With the ResourceBundle, this is done through a ResourceManagerInterface implementation. If using Doctrine, you can use the bundled ResourceManager
. Internally it uses Doctrine's ManagerRegistry
to get the correct object manager for the resource.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... myapp.resource.manager: class: ProgrammingAreHard\ResourceBundle\Domain\Manager\Doctrine\ResourceManager arguments: - @doctrine
Resource Forms
The bundle makes use of Symfony's form component to map incoming data to resources. Time to create a form for our Task
.
<?php // src/MyApp/CoreBundle/Domain/Task/Form/Type/TaskType.php namespace MyApp\CoreBundle\Domain\Task\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class TaskType extends AbstractType { private $class; public function __construct($class) { $this->class = $class; } public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('task'); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => $this->class, )); } public function getName() { return 'task'; } }
By default, the bundle attempts to find a resource's form by looking for a form with the name of the resource's class
that has been lowercased and underscored. Ie. The bundle would expect to find a form by the name of todo_list
for a MyApp/CoreBundle/Entity/TodoList
resource.
Let's register the form with the container.
# src/MyApp/CoreBundle/Resources/services.yml parameters: myapp.task.entity.class: MyApp\CoreBundle\Entity\Task services: # other services... myapp.task.form.type: class: MyApp\CoreBundle\Domain\Task\Form\Type\TaskType arguments: - %myapp.task.entity.class% tags: - { name: form.type, alias: task }
Form Handlers
Now that we have a resource, repository, and form it's time to create an implementation of a FormHandlerInterface
.
Form handlers are only executed if a request was issued and the form was valid. The bundle comes with a SaveResourceFormHandler
.
It extracts the data(the resource from the form) and saves it through a ResourceManagerInterface
. Let's register a resource
form handler in the container.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... myapp.resource.form_handler: class: ProgrammingAreHard\ResourceBundle\Domain\Form\Handler\SaveResourceFormHandler arguments: - @myapp.resource.manager
##Form Processors We need to use our new handler in a form processor.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... myapp.resource.form_processor: class: ProgrammingAreHard\ResourceBundle\Domain\Form\FormProcessor arguments: - @myapp.resource.form_handler - @pah_resource.form.error_extractor
Form processors use a form handler if the form is valid and an error extractor for when it is invalid. If you do not like the default form error extractor, you can create your own by implementing the FormErrorExtractorInterface.
##Resource Controllers
The glue that brings all these pieces together is the abstract ResourceController.
Let's create a concrete TaskController
.
<?php // src/MyApp/CoreBundle/Controller/TaskController.php namespace MyApp\CoreBundle\Controller; use MyApp\CoreBundle\Entity\Task; use ProgrammingAreHard\ResourceBundle\Controller\ResourceController; use ProgrammingAreHard\ResourceBundle\Domain\Event\ResourceEvents; use ProgrammingAreHard\ResourceBundle\Domain\ResourceInterface; class TaskController extends ResourceController { /** * Task class. * * @var string */ protected $resourceClass = Task::CLASS; // using php 5.5's class constant /** * Show current user's tasks. * * @return \Symfony\Component\HttpFoundation\Response */ public function indexAction() { $tasks = $this->getUser()->getTasks(); foreach ($tasks as $task) { $this->getResourceEventDispatcher()->dispatch(ResourceEvents::PRE_VIEW, $task); } return $this ->setData([$this->getPluralizedResourceName() => $tasks]) ->respond(); } /** * {@inheritdoc} */ protected function getResourceLocation(ResourceInterface $resource) { return $this->generateUrl('my_app_task_view', ['id' => $resource->getId()]); } /** * {@inheritdoc} */ protected function getFormProcessor() { return $this->get('myapp.resource.form_processor'); } /** * {@inheritdoc} */ protected function getResourceManager() { return $this->get('myapp.resource.manager'); } /** * {@inheritdoc} */ protected function getResourceRepository() { return $this->get('myapp.task.repository'); } }
Note: The $resourceClass
is used by the ResourceController
to find the relevant form, naming events, and serializing resources.
Tip: You might want to create your own base ResourceController
and implement ::getFormProcessor()
and ::getResourceManager()
as they will probably be the same across each of your resource controllers.
Because the ResourceController
uses symfony's security component to check basic REST permissions, we need to implement
a security voter. You can customize this to suit your application's needs. For now, we're going to allow everything.
<?php //src/MyApp/CoreBundle/Domain/Resource/Security/Voter/ResourceVoter.php namespace MyApp\CoreBundle\Domain\Resource\Security\Voter; use ProgrammingAreHard\ResourceBundle\Domain\ResourceInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; class ResourceVoter implements VoterInterface { /** * {@inheritdoc} */ public function supportsAttribute($attribute) { return true } /** * {@inheritdoc} */ public function supportsClass($class) { return true } /** * {@inheritdoc} */ public function vote(TokenInterface $token, $resource, array $attributes) { // Your application's logic to determine if the user has permission // to perform 'VIEW', 'CREATE', 'UPDATE', and/or 'DELETE' permissions. if ($resource instanceof ResourceInterface) { return VoterInterface::ACCESS_GRANTED; } return VoterInterface::ACCESS_ABSTAIN; } }
Don't forget to register it.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... myapp.resource.security.voter: class: MyApp\CoreBundle\Domain\Resource\Security\Voter\ResourceVoter public: false tags: - { name: security.voter }
##Resource Routing
Once we have our TaskController
and our security voter set up, we can then RESTfully route to actions.
# app/config/routing.yml # other routes... myapp_tasks: resource: "@MyAppCoreBundle/Resources/config/routing.yml" prefix: /api
# src/MyApp/CoreBundle/Resources/config/routing.yml myapp_task_create: pattern: /tasks defaults: { _controller: MyAppCoreBundle:Task:create } methods: [POST] myapp_task_view_all: pattern: /tasks defaults: { _controller: MyAppCoreBundle:Task:index } methods: [GET] myapp_task_view: pattern: /tasks/{id} defaults: { _controller: MyAppCoreBundle:Task:show } methods: [GET] myapp_task_update: pattern: /tasks/{id} defaults: { _controller: MyAppCoreBundle:Task:update } methods: [PUT] myapp_task_delete: pattern: /tasks/{id} defaults: { _controller: MyAppCoreBundle:Task:delete } methods: [DELETE]
I highly recommend you take a peek at the ResourceController to see what's happening under the hood.
By default, the ResourceBundle uses Symfony's serializer component to serialize resources for responses. However, I recommend using the JMSSerializerBundle for more flexibility.
##Events
The bundle's components are developed in a manner to make it easy to add functionality. One important piece of functionality is the ability to dispatch events. Events can be dispatched by wrapping certain components in decorators. The bundle comes with three.
###Eventful Resource Manager You can use this decorator to dispatch events during manager interactions.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... myapp.resource.eventful_manager: class: ProgrammingAreHard\ResourceBundle\Domain\Manager\Decorator\EventfulResourceManager arguments: - @myapp.resource.manager - @pah_resource.resource.event_dispatcher
By using this decorator, the following events will be dispatched:
- task.pre_save
- task.post_save
- task.pre_create
- task.post_create
- task.pre_update
- task.post_update
- task.pre_delete
- task.post_delete
It uses the ResourceEventDispatcher to dispatch these events. It uses the same class transformer as the ResourceController does when finding a resource's form. It lowercases and underscores a resource's class to use in the event name. Feel free to use the resource event dispatcher in your own code(like in your event listeners).
###Eventful Form Handler You can decorate your form handlers to dispatch pre and post handle events.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... # redefine to use the eventful manager myapp.resource.form_handler: class: ProgrammingAreHard\ResourceBundle\Domain\Form\Handler\SaveResourceFormHandler arguments: - @myapp.resource.eventful_manager # redefine to use the above form handler myapp.resource.eventful_form_handler: class: ProgrammingAreHard\ResourceBundle\Domain\Form\Decorator\EventfulFormHandler arguments: - @myapp.resource.form_handler - @pah_resource.form.event_dispatcher # redefine to use the above eventful form handler myapp.resource.form_processor: class: ProgrammingAreHard\ResourceBundle\Domain\Form\FormProcessor arguments: - @myapp.resource.eventful_form_handler - @pah_resource.form.error_extractor
Using this decorator will dispatch the following events for the task's form handler:
- task.form.pre_handle
- task.form.post_handle
###Eventful Form Processor You can also decorate form processors to dispatch certain events throughout the form processing.
# src/MyApp/CoreBundle/Resources/services.yml services: # other services... myapp.resource.eventful_form_processor: class: ProgrammingAreHard\ResourceBundle\Domain\Form\Decorator\EventfulFormProcessor arguments: - @myapp.resource.form_processor - @pah_resource.form.event_dispatcher
Using this decorator will dispatch the following events for the tasks's form.
- task.form.initialize
- task.form.invalid (only if the form is invalid)
- task.form.complete (only if the form is valid)
To summarize, by taking advantage of these decorators you will have access to the following events:
- task.pre_save
- task.post_save
- task.pre_create
- task.post_create
- task.pre_update
- task.post_update
- task.pre_delete
- task.post_delete
- task.form.initialize
- task.form.invalid
- task.form.pre_handle
- task.form.post_handle
- task.form.complete
Don't forget to use them in your ResourceController
s though!
##Bundle Configuration Reference This is the default bundle configuration.
# app/config/config.yml programming_are_hard_resource: class_transformer: pah_resource.class_name.underscore_transformer form_error_extractor: pah_resource.form.flattened_error_extractor
The class_tranformer
is responsible for turning a resource's fully qualified class name into a name it uses when
finding a resource's form and dispatching events. It must implement TransformerInterface.
The form_error_extractor
is responsbile for getting errors from a form. It must implement FormErrorExtractorInterface
##Special Notes There are a few things to keep in mind when using this bundle.
###IndexAction
You might have noticed that there is no indexAction
in the ResourceController
. This is because the bundle can't reasonably guess all of the required parameters needed to come up with a generic enough solution. For example, most likely you will only want to display the current user's resources rather than everything in a resource repository. You may also want advanced url structures, ie. /api/people/24/tasks
. The indexAction
method signature we need to accommodate that person id wildcard. Consequently, pagination and filtering are left up to you.
###ResourceEvents::PRE_VIEW
This event is dispatched in the create
, show
, and update
actions in the ResourceController
. Depending on how you display related resources, you may want to ignore these events. You can use a serializer to display a resource's related resources. These resources would be fetched by calling getters on the resources themselves. Consequently, there is no place to dispatch the ResourceEvents::PRE_VIEW
events for the related resources and still keep the bundle ORM agnostic. One option is to pull in the BazingaHateoasBundle and configure links for related resources. This way you can be sure that individual resources are only ever displayed directly by the ResourceController
.