Latest release: v0.4

Apitte/Core

Content

Installation

Simplest way to register this core API library is via Nette\DI\CompilerExtension.

composer require apitte/core
1
extensions:
    api: Apitte\Core\DI\ApiExtension
1
2

Configuration

extensions:
    api: Apitte\Core\DI\ApiExtension

api:
    debug: %debugMode%
1
2
3
4
5

By default, debug mode is detected from %debugMode% variable from Nette. Also there are default plugins Apitte\Core\DI\Plugin\CoreSchemaPlugin and Apitte\Core\DI\Plugin\CoreServicesPlugin loaded.

You can read more about plugins in the next chapter.

Usage

Controllers

Your job is to create a couple of controllers representing your API. Let's take a look at one.

namespace App\Controllers;

use Apitte\Core\Annotation\Controller\Controller;
use Apitte\Core\Annotation\Controller\ControllerPath;
use Apitte\Core\Annotation\Controller\Method;
use Apitte\Core\Annotation\Controller\Path;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use Apitte\Core\UI\Controller\IController;

/**
 * @Controller
 * @ControllerPath("/hello")
 */
final class HelloController implements IController
{

    /**
     * @Path("/world")
     * @Method("GET")
     */
    public function index(ApiRequest $request, ApiResponse $response): ApiResponse
    {
        return $response->writeBody('Hello world!');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

This API by automatic look for all services which implements Apitte\Core\UI\Controller\IController. Then they are analyzed by annotations loader and Apitte\Core\Schema\ApiSchema is build.

You have to mark your controllers with @Controller annotation and also define @ControllerPath.

Each public method with annotations @Path and @Method will be added to our API scheme and will be triggered in propel request.

One more thing left, you have to define your controllers as services, to let Apitte\Core\Handler\ServiceHandler obtain propel handler.

services:
    - App\Controllers\HelloController
1
2

At the end, open your browser and locate to localhost/<api-project>/hello/worldd.

Tip The @ControllerPath("/") annotation with the @Path("/") annotation target to homepage, e.q. localhost/<api-project>.

Request & Response

Apitte\Core\Http\ApiRequest & Apitte\Core\Http\ApiResponse implement the PSR-7 interfaces.

Annotations

Annotation Target Attributes Description
@Controller Class none Mark as as type controller.
@ControllerId Class value={a-z, A-Z, 0-9, _} Prefix all children methods ids with id.
@ControllerPath Class value={a-z, A-Z, 0-9, -_/} Prefix all children methods paths with path.
@GroupId Class value={a-z, A-Z, 0-9, _} Prefix all children methods ids with id. Can be set only on abstract class.
@GroupPath Class value={a-z, A-Z, 0-9, -_/} Prefix all children methods paths with path. Can be set only on abstract class.
@Id Method value={a-z, A-Z, 0-9, _} Set id to target method.
@Method Method GET, POST, PUT, OPTION, DELETE, HEAD Set method to target method.
@Negotiations Method @Negotiation Group annotation for @Negotiation.
@Negotiation Method suffix={string}, default={true/false}, renderer={string} Define negotiation mode to target method.
@Path Method value={a-z, A-Z, 0-9, -_/{}} Set path to target method. A.k.a. URL path.
@RequestParameters Method @RequestParameter Group annotation for @RequestParameter.
@RequestParameter Method name={string}, type={string/int/float/bool/datetime}, description={string}, in={path/query/header/cookie}, required={true/false}, deprecated={true/false}, allowEmpty={true/false} Define dynamic typed parameter.
@Tag Method name={string}, value={mixed} Add tag to target method.

Decorators

FileResponseDecorator

  • Transform response for simply send of file
use Apitte\Core\Response\Decorator\FileResponseDecorator;

$decorator = new FileResponseDecorator();
$response = $decorator->decorate(ResponseInterface $response, StreamInterface $data, string $filename);
1
2
3
4

Plugins

Apitte is divided into many plugins which are connected to one single awesome unit. The main apitte\core package is strongly required.

Core plugins are:

Another available plugins are:

CoreDecoratorPlugin

api:
    plugins:
        Apitte\Core\DI\Plugin\CoreDecoratorPlugin:
1
2
3

This plugin overrides default implementation of IDispatcher and allows to add request & response decorators. You can manage/update incoming request data or unify JSON response data via registered decorators.

Each decorator should be registered with tag apitte.core.decorator.

Each decorator should provide type attribute:

  • handle.before - called before controller method is triggered (after endpoint is matched in router)
  • handle.after - called after controller method is triggered (after logic in controller)
  • dispatcher.exception - called if exception has been occurred

Also you should define a priority for better sorting. Default is 10.

services:
    decorator.request.json:
        class: App\Model\JsonBodyDecorator
        tags: [apitte.core.decorator: [priority: 50, type: handler.before]]

services:
    decorator.request.xml:
        class: App\Model\XmlBodyDecorator
        tags: [apitte.core.decorator: [priority: 60, type: handler.before]]
1
2
3
4
5
6
7
8
9

When the DIC is compiled, we have a 2 decorators, the first is @decorator.request.json, because it has priority 50 and the second is @decorator.request.xml. Both of them are called before dispatching.

Default decorators

These decorators are registered by default. Be careful about priorities.

Plugin Class Type Priority Description
core RequestParametersDecorator handler.before 100 Enable @RequestParameter(s)
core RequestEntityDecorator handler.before 101 Enable @RequestMapper
negotiation ResponseEntityDecorator handler.after 500 Enable @ResponseMapper
Converts response entity to different formats

CoreMappingPlugin

api:
    plugins:
        Apitte\Core\DI\Plugin\CoreMappingPlugin:
          types: []
          request:
            validator: Apitte\Core\Mapping\Validator\NullValidator
1
2
3
4
5
6

Types

This plugin allows you to define annotation @RequestParameter which validates and converts data type of GET parameters.

/**
 * @Path("/user/{id}")
 * @Method("GET")
 * @RequestParameters({
 *      @RequestParameter(name="id", type="int", description="My favourite user ID")
 * })
 */
public function detail(ApiRequest $request)
{
    $id = $request->getParameter('id');
    // $id === int
}
1
2
3
4
5
6
7
8
9
10
11
12

Available data types are string, int, float, bool and datetime.

  • string
    • Simply returns given value.
  • int
    • Converts value to int.
    • Could overflow to float if value is bigger than PHP could handle. If it is your case then replace IntegerTypeMapper with your own implementation.
  • float
    • Converts value to float.
    • Accepts values which have decimals divided by comma , or dot .
  • bool
    • Converts 'true' to true
    • and 'false' to false
  • datetime
    • Converts value to DateTimeImmutable.
  • Each of the data types could return null if @RequestParameter(allowEmpty=true)
  • If conversion is not possible so API returns HTTP 400

You can override these types by your own implementation.

api:
    plugins:
        Apitte\Core\DI\Plugin\CoreMappingPlugin:
            types:
                string: Apitte\Core\Mapping\Parameter\StringTypeMapper
                int: Apitte\Core\Mapping\Parameter\IntegerTypeMapper
                float: Apitte\Core\Mapping\Parameter\FloatTypeMapper
                bool: Apitte\Core\Mapping\Parameter\BooleanTypeMapper
                datetime: Apitte\Core\Mapping\Parameter\DateTimeTypeMapper
1
2
3
4
5
6
7
8
9

Entity

RequestMapper

Let's try to picture you have a datagrid with many filter options. You can describe all options manually or use value object, entity, for it. And it leads us to @RequestMapper.

We have some entity with described fields.

namespace App\Controllers\Entity\Request;

use Apitte\Core\Mapping\Request\BasicEntity;

final class UserFilter extends BasicEntity
{

	/**  @var int */
	public $userId;

	/**  @var string */
	public $email;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

And some endpoint with @RequestMapper annotation. There's a method ApiRequest::getEntity(), it gets the entity from request attributes. So simple, right?

/**
 * @Path("/filter")
 * @Method("GET")
 * @RequestMapper(entity="App\Controllers\Entity\Request\UserFilter")
 */
public function filter(ApiRequest $request)
{
    $entity = $request->getEntity();
    // $entity === UserFilter
}
1
2
3
4
5
6
7
8
9
10

There's a prepared validator for request entity, but it's disabled by default. You have to pick the validator you want to.

api:
    plugins:
        Apitte\Core\DI\Plugin\CoreMappingPlugin:
          request:
            # By default
            validator: Apitte\Core\Mapping\Validator\NullValidator

            # Support: @required
            validator: Apitte\Core\Mapping\Validator\BasicValidator

            # Symfony/Validator
            validator: Apitte\Core\Mapping\Validator\SymfonyValidator(@phpdoc.reader)
1
2
3
4
5
6
7
8
9
10
11
12

If you want use SymfonyValidator so also register PhpdocExtension

extensions:
    phpdoc: Contributte\PhpDoc\DI\PhpDocExtension
1
2

Your entity could looks like this.

final class UserFilter extends BasicEntity
{

	/**
	 * @var int
	 * @Assert\NotNull()
	 * @Assert\Type(
	 *     type="integer",
	 *     message="The value {{ value }} is not a valid {{ type }}."
	 * )
	 */
	public $userId;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ResponseMapper

@ResponseMapper is almost same as @RequestMapper. It just applies to response instead of request.

You could use it to for example to filter values or convert output to different format.

namespace App\Controllers\Entity\Response;

use Apitte\Core\Mapping\Response\BasicEntity;

final class UserFilter extends BasicEntity
{

	/**  @var int */
	public $userId;

	/**  @var string */
	public $email;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * @Path("/filter")
 * @Method("GET")
 * @ResponseMapper(entity="App\Controllers\Entity\Response\UserFilter")
 */
public function filter(ApiRequest $request)
{
    return $request->getEntity();
}
1
2
3
4
5
6
7
8
9

Bridges

Middlewares

This API is mainly (but not required) based on contributte/middlewares. You should register also middleware extension in your config file.

extensions:
    middlewares: Contributte\Middlewares\DI\MiddlewaresExtension
    api: Apitte\Core\DI\ApiExtension
1
2
3

Resources

It's boring to register each controller one by one, let them register over the ResourceExtension. Install another contributte package - contributte/di.

And define your resources.

extensions:
    resource: Contributte\DI\Extension\ResourceExtension
    middlewares: Contributte\Middlewares\DI\MiddlewaresExtension
    api: Apitte\Core\DI\ApiExtension

resource:
    resources:
        App\Controllers\:
            # where the classes are stored
            paths: [%appDir%/controllers]
1
2
3
4
5
6
7
8
9
10

Playground

I've made a repository with full applications for education.

Take a look: https://github.com/apitte/playground