helmich / flow-resttools
Utility package for creating RESTful webservices with TYPO3 Flow
Installs: 88
Dependents: 1
Suggesters: 0
Security: 0
Stars: 5
Watchers: 5
Forks: 2
Open Issues: 0
Type:typo3-flow-package
Requires
- typo3/flow: *
This package is auto-updated.
Last update: 2020-01-27 18:47:45 UTC
README
Author
Martin Helmich typo3@martin-helmich.de
Synopsis
This package contains a set of helper classes for implementing RESTful webservices with TYPO3 Flow.
It specifically handles the following concerns:
- Controller configuration
- Serializing domain objects
- Request body handling
- Exception handling
Controller configuration
This package offers a RestController
that you may use when implementing controllers. Please note that this class
does not extend Flow's own RestController
class.
The RestController
class contains a default view configuration that enables
the controller to support JSON, YAML, XML and MSGPACK representations
(currently, output only. Input is still handled by Flow's default MediaConverter,
which supports only JSON and XML). Stay tuned for more.
Serializing domain objects
This package contains an API for converting domain objects into representations.
Unlike TYPO3 Flow's JsonView
, which relies on automatically building the
representation based on values returned by an object's getter method, this
package requires explicit normalizer classes to be supplied for each domain
model. I prefer this design because it offers more control on how object
representations are generated.
In general, object serialization is performed in two steps:
-
Normalization: Convert domain objects into a plain PHP array. This step is domain-specific. This means that you have to specify a Normalizer class for each domain object you want to present. These classes have to implement the
NormalizerInterface
(see source) and return a scalar PHP type -- usually a (nested) array. -
Serialization: Converts scalar PHP types generated by the normalization step into a string represantation. This step is not domain-specific. Currently, there are normalizers for JSON, YAML and MessagePack.
Request body handling
Motivation
One thing that's bugged me most about Flow is it's limited handling of request bodies. Deserializing JSON bodies works only when you wrap your resources in an envelope object that Flow can then map to the request arguments.
For instance, consider the following controller action:
public function testAction(Product $product) { // ... }
To successfully map the $product
argument, your JSON request body would also
need a "product"
property:
{ "product": { # ... } }
Solution
You can use the annotation Rest\BodyParam
to denote a controller action
argument that should be populated from the request body:
<?php namespace My\Example\RestApi\Controller; use Helmich\RestTools\Annotations as Rest; class TestController { /** * @Rest\BodyParam("$product", allowProperties={"name", "price"}) */ public function testAction(Product $product) { // ... } }
The allowProperties
key also pre-configures the property mapper to allow a
certain set of properties to be mapped (which means that you don't need to
explicitly enable this in an initialize
method.
Alternatively, you can use the allowAllProperties
key and set it to true
to
allow all properties to be mapped (use with caution, as this might expose a
security risk):
/** * @Rest\BodyParam("$product", allowAllProperties=TRUE) */ public function testAction(Product $product) { // ... }
Exception handling
This package overrides Flow's default exception handlers with it's own set of
exception handlers. Both error handlers (ProductionRestExceptionHandler
and
DevelopmentRestExceptionHandler
) present uncaught exceptions as JSON document
and attempt to guess an appropriate HTTP response code from the exception type
(for instance, a 400 status code will be thrown when an error during property
mapping occurred).
Complete example
Consider a simple domain object My\Example\Domain\Model\Product
with the
properties name and quantity.
First, implement a Normalizer for converting instances of these class to a scalar value:
<?php namespace My\Example\RestApi\Normalizer; use Helmich\RestTools\Rest\Normalizer\NormalizerInterface; use My\Example\Domain\Model\Product; use TYPO3\Flow\Annotations as Flow; use TYPO3\Flow\Persistence\PersistenceManagerInterface; class ProductNormalizer implements NormalizerInterface { /** * @var PersistenceManagerInterface * @Flow\Inject */ protected $persistenceManager; public function objectToScalar($object) { if ($object instanceof Product) { return [ 'id' => $this->persistenceManager->getIdentifierByObject($object), 'name' => $object->getName(), 'amount_in_stock' => $object->getQuantity() ]; } } }
In your controller, you can then wire this normalizer to your entity class:
<?php namespace My\Example\Controller; use My\Example\Domain\Model\Product; use My\Example\Domain\Repository\ProductRepository; use My\Example\RestApi\Normalizer\ManufacturerNormalizer; use Helmich\RestTools\Annotations as Rest; use Helmich\RestTools\Mvc\Controller\RestController; use Helmich\RestTools\Mvc\View\SerializingViewInterface; use TYPO3\Flow\Annotations as Flow; use TYPO3\Flow\Mvc\View\ViewInterface; class ProductController extends RestController { /** * @var ProductRepository * @Flow\Inject */ protected $productRepository; public function initializeView(ViewInterface $view) { if ($view instanceof SerializingViewInterface) { $view->registerNormalizerForClass(Product::class, new new ProductNormalizer()); } } public function listAction() { if ($view instanceof SerializingViewInterface) { $this->view->setRootElement('products'); } $this->view->assign('products', $this->productRepository->findAll()); } /** * @Rest\BodyParam("$product", allowAllProperties=TRUE) */ public function createAction(Product $product) { $this->productRepository->add($product); } }