helmich/flow-resttools

This package is abandoned and no longer maintained. No replacement package was suggested.
There is no license information available for the latest version (dev-master) of this package.

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

dev-master 2016-01-26 15:00 UTC

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);
  }
}