enova / slim-skeleton
Template para generación de microservicios
Requires
- php: >=7.3.0
- enova/cache: v1.0.0
- enova/core: v1.0.0
- enova/router: v1.0.0
- illuminate/database: 5.8.*
- illuminate/events: 5.8.*
- illuminate/filesystem: 5.8.15
- illuminate/translation: 5.8.15
- illuminate/validation: 5.8.15
- league/fractal: 0.17.*
- monolog/monolog: 1.23.*
- robmorgan/phinx: 0.10.*
- slim/slim: 3.12.1
- symfony/console: 4.2.7
- tuupola/cors-middleware: 1.0.0
- tuupola/slim-jwt-auth: 3.1.*
- vlucas/phpdotenv: ^2.4
- webpatser/laravel-uuid: 2.2.1
Requires (Dev)
- phpunit/phpunit: 8.1.5
This package is auto-updated.
Last update: 2025-03-20 09:03:13 UTC
README
Use this skeleton application for the slim 3 micro-framework with some pre-configured dependencies and structures:
- Eloquent as ORM for database work
- Monolog for logging
- Silly CLI micro-framework for CLI Commands
- vlucas/phpdotenv to load environment configuration from ".env" file
- Symfony/cache as simple file cache scale to cache based on NoSQL Systems
- Fractal presentation and transformation layer.
- Controller, Middleware and Factory classes
- Exception handling
- Controllers based on annotations
- Migration system with phinx and eloquent by laravel
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 asLeague\Fractal\Pagination\IlluminatePaginatorAdapter
- The
pagerfanta/pagerfanta
package asLeague\Fractal\Pagination\PagerfantaPaginatorAdapter
- Zend Framework’s
zendframework/zend-paginator
package asLeague\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
tonext
from the last response - Set
previous
tocurrent
from the last response limit
is optional * You can set it tocount
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
}
}
}