enova/slim-skeleton

Template para generaciĆ³n de microservicios

v1.0.0 2019-06-25 17:15 UTC

README

Use this skeleton application for the slim 3 micro-framework with some pre-configured dependencies and structures:

Requirements if you don want to use Enova-Skeleton

  • PHP >= 7.3
  • Composer
  • Docker

Create a new Application using Enova-Skeleton

Run this command from the directory in which you want to install your new Enova Slim Framework application.

    composer create-project enova/slim-skeleton [my-app-name]

Replace [my-app-name] with the desired directory name for your new application. You'll want to:

  • Point your virtual host document root to your new application's public/ directory.
  • Ensure storage/ is web writeable.
  • create a copy ".env" of the file ".env.example" an set up your configuration

Run on devlopment environment

  • type composer start and your service is going to start in the port 7000

Production or development environment using docker

first we must to create a docker image from Dockerfile:

docker build -t [image's name]:[version] .

Finally run the container:

docker run --name calendar-service -p 7000:80 -v $(pwd):/var/www/microservice --net=net-calendar-service -e TZ="America/Mexico_City" -d --restart=always calendar-service:1.0

Create Migrations

Phinx is a database migration system

Writing new migrations

If you need create a new migration to transform the database, the first step to create a new migraton is generate a sketelon migration file.

Let’s start by creating a new Phinx migration. Run Phinx using the create command:

$ vendor/bin/phinx create MyNewMigration

This will create a new migration in the format YYYYMMDDHHMMSS_my_new_migration.php, where the first 14 characters are replaced with the current timestamp down to the second.

If you have specified multiple migration paths, you will be asked to select which path to create the new migration in.

Phinx automatically creates a skeleton migration file with a single method:

<?php


use Enova\Utils\Commons\Db\Migration;

class InitSomething extends Migration
{
    /**
     * Change Method.
     *
     * Write your reversible migrations using this method.
     *
     * More information on writing migrations is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
     *
     * The following commands can be used in this method and Phinx will
     * automatically reverse them when rolling back:
     *
     *    createTable
     *    renameTable
     *    addColumn
     *    addCustomColumn
     *    renameColumn
     *    addIndex
     *    addForeignKey
     *
     * Any other destructive changes will result in an error when trying to
     * rollback the migration.
     *
     * Remember to call "create()" or "update()" and NOT "save()" when working
     * with the Table class.
     */
    public function change()
    {

    }
}

All Enova Phinx migrations extend from the Migration class. This class provides the necessary support to create your database migrations. Database migrations can transform your database in many ways, such as creating new tables, inserting rows, adding indexes and modifying columns.

The Change Method

Phinx 0.2.0 introduced a new feature called reversible migrations. This feature has now become the default migration method. With reversible migrations, you only need to define the up logic, and Phinx can figure out how to migrate down automatically for you. Phinx will automatically ignore the up and down methods. If you need to use these methods it is recommended to create a separate migration file.

The Up Method

The up method is automatically run by Phinx when you are migrating up and it detects the given migration hasn’t been executed previously. You should use the up method to transform the database with your intended changes.

The Down Method

The down method is automatically run by Phinx when you are migrating down and it detects the given migration has been executed in the past. You should use the down method to reverse/undo the transformations described in the up method.

Creating a table

Creating table is really easy using the schema object

<?php


use Enova\Mako\Lib\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CatalogCfdi extends Migration
{
    public function up()
    {
        $this->schema->create('catalog', function(Blueprint $table){
            $table->increments('id');
            $table->char('c_aduana', 2);
            $table->string('descripcion', 150);
        });

        $this->schema->create('catalog_clave_prod_serv', function(Blueprint $table){
            $table->increments('id');
            $table->char('c_clave_prodserv', 8);
            $table->string('descripcion', 300);
            $table->dateTime('fecha_inicio_vigencia');
            $table->dateTime('fecha_fin_vigencia')->nullable();
            $table->string('incluir_iva_trasladado', 15);
            $table->string('incluir_ieps_trasladados', 15);
            $table->string('complemento_que_debe_incluir', 50)->nullable();
        });
    }
    
    public function down()
    {
        $this->schema->dropIfExists('catalog_aduana');
        $this->schema->dropIfExists('catalog_clave_prod_serv');
    }
}

Fractal

What is Fractal?

Fractal provides a presentation and transformation layer for complex data output, the like found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc.

When building an API it is common for people to just grab stuff from the database and pass it to json_encode(). This might be passable for “trivial” APIs but if they are in use by the public, or used by mobile applications then this will quickly lead to inconsistent output.

Goals

  • Create a “barrier” between source data and output, so schema changes do not affect users
  • Systematic type-casting of data, to avoid foreach()ing through and (bool)ing everything
  • Include (a.k.a embedding, nesting or side-loading) relationships for complex data structures
  • Work with standards like HAL and JSON-API but also allow custom serialization
  • Support the pagination of data results, for small and large data sets alike
  • Generally ease the subtle complexities of outputting data in a non-trivial API

Glossary

Learn more about the general concepts of Fractal.

Cursor

A cursor is an unintelligent form of Pagination, which does not require a total count of how much data is in the 
database. This makes it impossible to know if the "next" page exists, meaning an API client would need to keep 
making HTTP Requests until no data could be found (404).

Include

Data usually has relationships to other data. Users have posts, posts have comments, comments belong to posts, etc. 
When represented in RESTful APIs this data is usually "included" (a.k.a embedded or nested) into the resource. 
A transformer will contain includePosts() methods, which will expect a resource to be returned, so it can be placed 
inside the parent resource.

Manager

Fractal has a class named Manager, which is responsible for maintaining a record of what embedded data has been 
requested, and converting the nested data into arrays, JSON, YAML, etc. recursively.

Pagination

Pagination is the process of dividing content into pages, which in relation to Fractal is done in two alternative 
ways: Cursors and Paginators.

Paginator

A paginator is an intelligent form of Pagination, which will require a total count of how much data is in the 
database. This adds a "paginator" item to the response meta data, which will contain next/previous links when 
applicable.

Resource

A resource is an object which acts as a wrapper for generic data. A resource will have a transformer attached, 
for when it is eventually transformed ready to be serialized and output.

Serializer

A Serializer structures your Transformed data in certain ways. There are many output structures for APIs, two 
popular ones being HAL and JSON-API. Twitter and Facebook output data differently to each other, and Google does 
it differently too. Serializers let you switch between various output formats with minimal effect on your 
Transformers.

Transformer

Transformers are classes, or anonymous functions, which are responsible for taking one instance of the resource 
data and converting it to a basic array. This process is done to obfuscate your data store, avoiding 
Object-relational impedance mismatch and allowing you to even glue various elements together from different data 
stores if you wish. The data is taken from these complex data store(s) and made into a format that is more 
manageable, and ready to be Serialized. 

Concepts

Resources

Resources are objects that represent data, and have knowledge of a “Transformer”, which is an object or callback that will know how to output the data.

Two types of resource exist:

  • League\Fractal\Resource\Item - A singular resource, probably one entry in a data store
  • League\Fractal\Resource\Collection - A collection of resources

The Item and Collection constructors will take any kind of data you wish to send it as the first argument, and then a “transformer” as the second argument.

Serializers

A Serializer structures your Transformed data in certain ways. There are many output structures for APIs, two popular ones being HAL and JSON-API. Twitter and Facebook output data differently to each other, and Google does it differently too. Most of the differences between these serializers are how data is namespaced.

Serializer classes let you switch between various output formats with minimal effect on your Transformers.

JsonApiSerializer

This is a representation of the JSON-API standard (v1.0). It implements the most common features such as

  • Primary Data
  • Resource Objects
  • Resource Identifier Objects
  • Compound Documents
  • Meta Information
  • Links
  • Relationships
  • Inclusion of Related Resources

Features that are not yet included

  • Sparse Fieldsets
  • Sorting
  • Pagination
  • Filtering

As Fractal is a library to output data structures, the serializer can only transform the content of your HTTP response. Therefore, the following has to be implemented by you

  • Content Negotiation
  • HTTP Response Codes
  • Error Objects

For more information please refer to the official JSON API specification.

JSON API requires a Resource Key for your resources, as well as an id on every object.

Custom Serializers

You can make your own Serializers by implementing SerializerAbstract.

Transformers

Classes for Transformers

To reuse transformers (recommended) classes can be defined, instantiated and passed in place of the callback.

These classes must extend League\Fractal\TransformerAbstract and contain at the very least a method with the name transform().

The method declaration can take mixed input, just like the callbacks:

<?php
namespace Acme\Transformer;

use Acme\Model\Book;
use League\Fractal;

class BookTransformer extends Fractal\TransformerAbstract
{
	public function transform(Book $book)
	{
	    return [
	        'id'      => (int) $book->id,
	        'title'   => $book->title,
	        'year'    => (int) $book->yr,
            'links'   => [
                [
                    'rel' => 'self',
                    'uri' => '/books/'.$book->id,
                ]
            ],
	    ];
	}
}

Once the Transformer class is defined, it can be passed as an instance in the resource constructor.

<?php
use Acme\Transformer\BookTransformer;
use League\Fractal;

$resource = new Fractal\Resource\Item($book, new BookTransformer);
$resource = new Fractal\Resource\Collection($books, new BookTransformer);

Including Data

Your transformer at this point is mainly just giving you a method to handle array conversion from your data source (or whatever your model is returning) to a simple array. Including data in an intelligent way can be tricky as data can have all sorts of relationships. Many developers try to find a perfect balance between not making too many HTTP requests and not downloading more data than they need to, so flexibility is also important.

Sticking with the book example, the BookTransformer, we might want to normalize our database and take the two author_* fields out and put them in their own table. This include can be optional to reduce the size of the JSON response and is defined like so:

<?php namespace App\Transformer;

use Acme\Model\Book;
use League\Fractal\TransformerAbstract;

class BookTransformer extends TransformerAbstract
{
    /**
     * List of resources possible to include
     *
     * @var array
     */
    protected $availableIncludes = [
        'author'
    ];

    /**
     * Turn this item object into a generic array
     *
     * @return array
     */
    public function transform(Book $book)
    {
        return [
            'id'    => (int) $book->id,
            'title' => $book->title,
            'year'    => (int) $book->yr,
            'links'   => [
                [
                    'rel' => 'self',
                    'uri' => '/books/'.$book->id,
                ]
            ],
        ];
    }

    /**
     * Include Author
     *
     * @return \League\Fractal\Resource\Item
     */
    public function includeAuthor(Book $book)
    {
        $author = $book->author;

        return $this->item($author, new AuthorTransformer);
    }
}

These includes will be available but can never be requested unless the Manager::parseIncludes() method is called:

<?php
use League\Fractal;

$fractal = new Fractal\Manager();

if (isset($_GET['include'])) {
    $fractal->parseIncludes($_GET['include']);
}

With this set, include can do some great stuff. If a client application were to call the URL /books?include=author then they would see author data in the response.

These includes can be nested with dot notation too, to include resources within other resources.

E.g: /books?include=author,publishers.somethingelse

Note: publishers will also be included with somethingelse nested under it. This is shorthand for publishers,publishers.somethingelse.

This can be done to a limit of 10 levels. To increase or decrease the level of embedding here, use the Manager::setRecursionLimit(5) method with any number you like, to strip it to that many levels. Maybe 4 or 5 would be a smart number, depending on the API.

Default Includes

Just like with optional includes, default includes are defined in a property on the transformer:

<?php namespace App\Transformer;

use Acme\Model\Book;
use League\Fractal\TransformerAbstract;

class BookTransformer extends TransformerAbstract
{
    /**
     * List of resources to automatically include
     *
     * @var array
     */
    protected $defaultIncludes = [
        'author'
    ];

    // ....

    /**
     * Include Author
     *
     * @param Book $book
     * @return \League\Fractal\Resource\Item
     */
    public function includeAuthor(Book $book)
    {
        $author = $book->author;

        return $this->item($author, new AuthorTransformer);
    }
}

This will look identical in output as if the user requested ?include=author. Excluding Includes

The Manager::parseExcludes() method is available for odd situations where a default include should be omitted from a single response.

<?php
use League\Fractal;

$fractal = new Fractal\Manager();

$fractal->parseExcludes('author');

The same dot notation seen for Manager::parseIncludes() can be used here.

Only the mostly deeply nested resource from the exclude path will be omitted.

To omit both the default author include on the BookTransformer and a default editor include on the nested AuthorTransformer, author.editor,author would need to be passed, since author.editor alone will omit only the editor resource from the respone.

Parsed exclusions have the final say whether or not an include will be seen in the response data. This means they can also be used to omit an available include requested in Manager::parseIncludes().

Include Parameters

When including other resources, syntax can be used to provide extra parameters to the include methods. These parameters are constructed in the URL, ?include=comments:limit(5|1):order(created_at|desc).

This syntax will be parsed and made available through a League\Fractal\ParamBag object, passed into the include method as the second argument.

<?php

use League\Fractal\ParamBag;

    // ... transformer stuff ...

    private $validParams = ['limit', 'order'];

    /**
     * Include Comments
     *
     * @param Book $book
     * @param \League\Fractal\ParamBag|null
     * @return \League\Fractal\Resource\Item
     */
    public function includeComments(Book $book, ParamBag $params = null)
    {
        if ($params === null) {
            return $book->comments;
        }

    	// Optional params validation
        $usedParams = array_keys(iterator_to_array($params));
        if ($invalidParams = array_diff($usedParams, $this->validParams)) {
            throw new \Exception(sprintf(
                'Invalid param(s): "%s". Valid param(s): "%s"', 
                implode(',', $usedParams), 
                implode(',', $this->validParams)
            ));
        }

    	// Processing
        list($limit, $offset) = $params->get('limit');
        list($orderCol, $orderBy) = $params->get('order');

        $comments = $book->comments
            ->take($limit)
            ->skip($offset)
            ->orderBy($orderCol, $orderBy)
            ->get();

        return $this->collection($comments, new CommentTransformer);
    }

Parameters have a name, then multiple values which are always returned as an array, even if there is only one. They are accessed by the get() method, but array access is also an option, so $params->get('limit') and $params['limit'] do the same thing.

Eager-Loading vs Lazy-Loading

The above examples happen to be using the lazy-loading functionality of an ORM for $book->author. Lazy-Loading can be notoriously slow, as each time one item is transformered, it would have to go off and find other data leading to a huge number of SQL requests.

Eager-Loading could easily be used by inspecting the value of $_GET['include'], and using that to produce a list of relationships to eager-load with an ORM.

Pagination

When working with a large data set it obviously makes sense to offer pagination options to the endpoint, otherwise that data can get very slow. To avoid writing your own pagination output into every endpoint, Fractal provides you with two solutions:

  • Paginator
  • Cursor

Using Paginators

Paginators offer more information about your result-set including total, and have next/previous links which will only show if there is more data available. This intelligence comes at the cost of having to count the number of entries in a database on each call.

For some data sets this might not be an issue, but for some it certainly will. If pure speed is an issue, consider using Cursors instead.

Paginator objects are created, and must implement League\Fractal\Pagination\PaginatorInterface and its specified methods. The instantiated object must then be passed to the League\Fractal\Resource\Collection::setPaginator() method.

Fractal currently ships with the following adapters:

  • Laravel’s illuminate/pagination package as League\Fractal\Pagination\IlluminatePaginatorAdapter
  • The pagerfanta/pagerfanta package as League\Fractal\Pagination\PagerfantaPaginatorAdapter
  • Zend Framework’s zendframework/zend-paginator package as League\Fractal\Pagination\ZendFrameworkPaginatorAdapter

Laravel Pagination

As an example, you can use Laravel’s Eloquent or Query Builder method paginate() to achieve the following:

use League\Fractal\Resource\Collection;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Acme\Model\Book;
use Acme\Transformer\BookTransformer;

$paginator = Book::paginate();
$books = $paginator->getCollection();

$resource = new Collection($books, new BookTransformer);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));

Symfony Pagination

Below is an example of pagination using the Pagerfanter Paginator with a collection of objects obtained from Doctrine.

$doctrineAdapter = new DoctrineCollectionAdapter($allItems);
$paginator = new Pagerfanta($doctrineAdapter);
$filteredResults = $paginator->getCurrentPageResults();

$paginatorAdapter = new PagerfantaPaginatorAdapter($paginator, function(int $page) use (Request $request, RouterInterface $router) {
	$route = $request->attributes->get('_route');
	$inputParams = $request->attributes->get('_route_params');
	$newParams = array_merge($inputParams, $request->query->all());
	$newParams['page'] = $page;
	return $router->generate($route, $newParams, 0);
});
$resource = new Collection($filteredResults, new BookTransformer);
$resource->setPaginator($paginatorAdapter);

Including existing query string values in pagination links

In the example above, previous and next pages will be provided simply with ?page=# ignoring all other existing query strings. To include all query string values automatically in these links we can replace the last line above with:

use Acme\Model\Book;

$year = Input::get('year');
$paginator = Book::where('year', '=', $year)->paginate(20);

$queryParams = array_diff_key($_GET, array_flip(['page']));
$paginator->appends($queryParams);

$paginatorAdapter = new IlluminatePaginatorAdapter($paginator);
$resource->setPaginator($paginatorAdapter);

Using Cursors

When we have large sets of data and running a SELECT COUNT(*) FROM whatever isn’t really an option, we need a proper way of fetching results. One of the approaches is to use cursors that will indicate to your backend where to start fetching results. You can set a new cursor on your collections using the League\Fractal\Resource\Collection::setCursor() method.

The cursor must implement League\Fractal\Pagination\CursorInterface and its specified methods.

Fractal currently ships with a very basic adapter: League\Fractal\Pagination\Cursor. It’s really easy to use:

use Acme\Model\Book;
use Acme\Transformer\BookTransformer;
use League\Fractal\Pagination\Cursor;
use League\Fractal\Resource\Collection;

$currentCursor = Input::get('cursor', null);
$previousCursor = Input::get('previous', null);
$limit = Input::get('limit', 10);

if ($currentCursor) {
    $books = Book::where('id', '>', $currentCursor)->take($limit)->get();
} else {
    $books = Book::take($limit)->get();
}

$newCursor = $books->last()->id;
$cursor = new Cursor($currentCursor, $previousCursor, $newCursor, $books->count());

$resource = new Collection($books, new BookTransformer);
$resource->setCursor($cursor);

These examples are for Laravel’s illuminate\database package, but you can do it however you like. The cursor also happens to be constructed from the id field, but it could just as easily be an offset number. Whatever is picked to represent a cursor, maybe consider using base64_encode() and base64_decode() on the values to make sure API users do not try and do anything too clever with them. They just need to pass the cursor to the new URL, not do any maths.

Example Cursor Usage

GET /books?cursor=5&limit=5

{
	"books": [
		{ "id": 6 },
		{ "id": 7 },
		{ "id": 8 },
		{ "id": 9 },
		{ "id": 10 }
	],
	"meta": {
		"cursor": {
			"previous": null,
			"current": 5,
			"next": 10,
			"count": 5
		}
	}
}

On the next request, we move the cursor forward.

  • Set cursor to next from the last response
  • Set previous to current from the last response
  • limit is optional * You can set it to count from the previous request to maintain the same limit

GET /books?cursor=10&previous=5&limit=5

{
	"books": [
		{ "id": 11 },
		{ "id": 12 },
		{ "id": 13 },
		{ "id": 14 },
		{ "id": 15 }
	],
	"meta": {
		"cursor": {
			"previous": 5,
			"current": 10,
			"next": 15,
			"count": 5
		}
	}
}