enova / slim-skeleton
微服务生成模板
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: 2024-09-20 07:56:34 UTC
README
使用此骨架应用程序为slim 3微框架提供一些预配置的依赖项和结构
- 作为数据库工作的ORM使用Eloquent
- 用于日志记录的Monolog
- 用于CLI命令的Silly CLI micro-framework
- 使用vlucas/phpdotenv从".env"文件加载环境配置
- 使用Symfony/cache作为简单的文件缓存,扩展到基于NoSQL系统的缓存
- 使用Fractal作为展示和转换层。
- 控制器、中间件和工厂类
- 异常处理
- 基于注解的控制器
- 使用phinx和eloquent(由laravel提供)的迁移系统
如果你想要使用Enova-Skeleton,以下是需要满足的要求
- PHP >= 7.3
- Composer
- Docker
使用Enova-Skeleton创建一个新的应用程序
从你想要安装新Enova Slim Framework应用程序的目录中运行此命令。
composer create-project enova/slim-skeleton [my-app-name]
将[my-app-name]
替换为你新应用程序希望使用的目录名称。你希望
- 将你的虚拟主机的文档根目录指向你的新应用程序的
public/
目录。 - 确保
storage/
是可写于网络的。 - 创建一个".env"文件的副本,并设置你的配置
在开发环境中运行
- 输入
composer start
,你的服务将在端口7000上启动
使用Docker的生成或开发环境
首先,我们必须从Dockerfile
创建一个Docker镜像
docker build -t [image's name]:[version] .
最后运行容器
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
创建迁移
Phinx是一个数据库迁移系统
编写新的迁移
如果你需要创建一个新迁移来转换数据库,创建新迁移的第一步是生成一个迁移文件框架。
让我们首先创建一个新的Phinx迁移。运行Phinx并使用create命令
$ vendor/bin/phinx create MyNewMigration
这将创建一个新的迁移,格式为YYYYMMDDHHMMSS_my_new_migration.php,其中前14个字符被当前时间戳(精确到秒)替换。
如果你指定了多个迁移路径,你将被要求选择在哪个路径下创建新迁移。
Phinx自动创建一个只有一个方法的迁移文件框架
<?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()
{
}
}
所有Enova Phinx迁移都扩展自Migration类。这个类提供了创建你的数据库迁移所需的支持。数据库迁移可以以许多方式转换数据库,例如创建新表、插入行、添加索引和修改列。
更改方法
Phinx 0.2.0引入了一个名为可逆迁移的新特性。现在,这个特性已经成为默认的迁移方法。使用可逆迁移,你只需要定义up逻辑,Phinx就可以自动为你找出如何自动回滚。Phinx将自动忽略up和down方法。如果你需要使用这些方法,建议创建一个单独的迁移文件。
Up方法
Phinx 在执行向上迁移时会自动运行 up 方法,并在检测到指定的迁移尚未执行过时进行执行。您应该使用 up 方法来根据您的意图对数据库进行转换。
下移方法
Phinx 在您执行向下迁移时会自动运行 down 方法,并在检测到指定的迁移曾经执行过时进行执行。您应该使用 down 方法来撤销 up 方法中描述的转换。
创建表
使用 schema 对象 创建表 真的非常简单
<?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');
}
}
分形
什么是分形?
分形 为复杂数据输出提供了一种表示和转换层,类似于 RESTful API 中的数据,并且与 JSON 兼容得很好。可以将它视为 JSON/YAML 等的视图层。
在构建 API 时,人们通常只是从数据库中抓取一些东西,然后传递给 json_encode()。这可能适用于“简单”的 API,但如果它们被公众使用或被移动应用程序使用,那么这很快会导致输出不一致。
目标
- 在源数据和输出之间创建“障碍”,以便架构更改不会影响用户
- 系统地转换数据类型,以避免对一切进行 foreach()ing 和 (bool)ing
- 包含(即内嵌、嵌套或侧加载)复杂数据结构的关系
- 与 HAL 和 JSON-API 等标准一起工作,但同时也允许自定义序列化
- 支持数据结果的分页,无论是小数据集还是大数据集
- 通常简化了在非简单 API 中输出数据的微妙复杂性
术语表
了解更多关于分形的一般概念。
光标
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).
包含
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.
管理器
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 is the process of dividing content into pages, which in relation to Fractal is done in two alternative
ways: Cursors and Paginators.
分页器
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.
资源
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.
序列化器
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.
转换器
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.
概念
资源
资源是代表数据的对象,并且知道一个“转换器”,这是一个对象或回调,知道如何输出数据。
存在两种类型的资源
- League\Fractal\Resource\Item - 单个资源,可能是数据存储中的一个条目
- League\Fractal\Resource\Collection - 资源集合
Item 和 Collection 构造函数将接受您希望发送的第一种数据类型,然后是一个“转换器”作为第二个参数。
序列化器
序列化器以某种方式结构化转换后的数据。API 有许多输出结构,两个流行的是 HAL 和 JSON-API。Twitter 和 Facebook 输出的数据不同,Google 的输出也不同。这些序列化器之间的差异大多在于数据命名空间。
序列化器类让您可以在对转换器影响最小的情况下在各种输出格式之间进行切换。
JsonApiSerializer
这是 JSON-API 标准的表示(v1.0)。它实现了最常用的功能,例如
- 主数据
- 资源对象
- 资源标识符对象
- 复合文档
- 元信息
- 链接
- 关系
- 包含相关资源
尚未包含的功能
- 稀疏字段集
- 排序
- 分页
- 过滤
由于 Fractal 是一个输出数据结构的库,因此序列化器只能转换您 HTTP 响应的内容。因此,以下内容必须由您实现
- 内容协商
- HTTP 响应代码
- 错误对象
有关更多信息,请参阅官方的 JSON API 规范。
JSON API 要求为您的资源提供资源键,以及每个对象的 id。
自定义序列化器
您可以通过实现 SerializerAbstract 来创建自己的序列化器。
转换器
转换器类
为了重用变压器(推荐),可以定义、实例化类,并用它们代替回调。
这些类必须扩展 League\Fractal\TransformerAbstract
,并且至少包含一个名为 transform()
的方法。
方法声明可以接受混合输入,就像回调一样。
<?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,
]
],
];
}
}
一旦定义了Transformer类,就可以在资源构造函数中以实例的形式传递。
<?php
use Acme\Transformer\BookTransformer;
use League\Fractal;
$resource = new Fractal\Resource\Item($book, new BookTransformer);
$resource = new Fractal\Resource\Collection($books, new BookTransformer);
包括数据
到目前为止,您的变压器主要为您提供一种方法来处理从数据源(或模型返回的任何内容)到简单数组的数组转换。以智能方式包括数据可能很复杂,因为数据可能有各种关系。许多开发者试图在不要进行太多的HTTP请求和不需要下载太多数据之间找到完美的平衡,因此灵活性也很重要。
继续使用书籍示例,即 BookTransformer
,我们可能想规范化我们的数据库,将两个 author_*
字段取出来,放入它们自己的表中。这个包含可以是可选的,以减少JSON响应的大小,定义如下
<?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);
}
}
这些包含将是可用的,但除非调用 Manager::parseIncludes()
方法,否则永远不会被请求。
<?php
use League\Fractal;
$fractal = new Fractal\Manager();
if (isset($_GET['include'])) {
$fractal->parseIncludes($_GET['include']);
}
有了这个设置,包含可以做很多事情。如果一个客户端应用程序调用URL /books?include=author,那么它们会在响应中看到作者数据。
这些包含也可以使用点符号进行嵌套,以在资源内包含其他资源。
例如:/books?include=author,publishers.somethingelse
注意:publishers
将包括嵌套在其下的 somethingelse
。这是 publishers,publishers.somethingelse
的简写。
这可以做到10级。要增加或减少嵌入级别,请使用 Manager::setRecursionLimit(5)
方法,并输入任何您喜欢的数字,以将其剥离到那么多级别。也许4或5会是API的智能数字。
默认包含
就像可选包含一样,默认包含是在转换器的属性上定义的。
<?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);
}
}
这将在输出上与用户请求 ?include=author
时看起来相同。排除包含
对于需要从单个响应中省略默认包含的异常情况,有 Manager::parseExcludes()
方法。
<?php
use League\Fractal;
$fractal = new Fractal\Manager();
$fractal->parseExcludes('author');
与 Manager::parseIncludes()
中的点符号相同,这里也可以使用。
将被省略的是排除路径中最深层嵌套的资源。
要省略 BookTransformer
上的默认 author
包含和嵌套的 AuthorTransformer
上的默认 editor
包含,需要传递 author.editor,author
,因为仅 author.editor
将仅从响应中省略 editor
资源。
解析的排除具有最终决定权,是否在响应数据中看到包含。这意味着它们还可以用于省略在 Manager::parseIncludes()
中请求的可用包含。
包含参数
当包含其他资源时,可以使用语法来提供额外的参数给包含方法。这些参数在URL中构建,?include=comments:limit(5|1):order(created_at|desc)
。
这个语法将被解析并通过 League\Fractal\ParamBag
对象提供,作为第二个参数传递给包含方法。
<?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);
}
参数有一个名称,然后有多个值,这些值始终以数组的形式返回,即使只有一个。它们可以通过 get()
方法访问,也可以使用数组访问,所以 $params->get('limit')
和 $params['limit']
做的是同一件事。
懒加载与预加载
上面的例子恰好使用了ORM的 $book->author
的懒加载功能。懒加载可能非常慢,因为每次转换一个项目时,它都必须去寻找其他数据,这会导致大量的SQL请求。
通过检查$_GET['include']
的值,可以轻松地使用预加载,并使用该值来生成一个与ORM一起预加载的关系列表。
分页
当处理大量数据集时,显然有必要向端点提供分页选项,否则数据会变得非常慢。为了避免在每一个端点中编写自己的分页输出,Fractal提供了两个解决方案
- 分页器
- 光标
使用分页器
分页器提供了有关结果集的更多信息,包括总数,并且有下一页/上一页链接,只有当有更多数据可用时才会显示。这种智能是以必须在每次调用中对数据库中的条目数量进行计数为代价的。
对于某些数据集来说,这可能不是问题,但对于某些数据集来说,这确实是一个问题。如果纯粹的速度是一个问题,考虑使用游标。
创建分页器对象,必须实现League\Fractal\Pagination\PaginatorInterface
及其指定方法。然后必须将实例化的对象传递给League\Fractal\Resource\Collection::setPaginator()
方法。
Fractal目前附带以下适配器
- Laravel的
illuminate/pagination
包作为League\Fractal\Pagination\IlluminatePaginatorAdapter
pagerfanta/pagerfanta
包作为League\Fractal\Pagination\PagerfantaPaginatorAdapter
- Zend Framework的
zendframework/zend-paginator
包作为League\Fractal\Pagination\ZendFrameworkPaginatorAdapter
Laravel分页
例如,您可以使用Laravel的Eloquent或查询构建器的paginate()
方法来实现以下功能
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分页
以下是一个使用来自Doctrine的对象集合的Pagerfanta分页器的分页示例。
$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);
将现有的查询字符串值包含在分页链接中
在上面的示例中,上一页和下一页将简单地提供?page=#
,忽略所有其他现有的查询字符串。为了自动将这些查询字符串值包含在这些链接中,我们可以用以下内容替换上面的最后一行
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);
使用游标
当我们有大量数据集,并且运行SELECT COUNT(*) FROM whatever
不是一个实际的选择时,我们需要一种正确获取结果的方法。一种方法是使用游标,这将指示您的后端从何处开始获取结果。您可以使用League\Fractal\Resource\Collection::setCursor()
方法在集合中设置新的游标。
游标必须实现League\Fractal\Pagination\CursorInterface
及其指定方法。
Fractal目前附带一个非常基础的适配器:League\Fractal\Pagination\Cursor
。它非常容易使用
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);
以下示例是为Laravel的illuminate\database
包,但您可以根据自己的喜好进行操作。游标实际上是由id
字段构建的,但它也可以是一个偏移量。无论选择什么来表示游标,也许可以考虑使用base64_encode()
和base64_decode()
对值进行编码和解码,以确保API用户不会对它们做任何过于复杂的事情。他们只需要将游标传递到新的URL,而不是进行任何数学计算。
游标使用示例
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
}
}
}
在下一次请求中,我们将游标向前移动。
- 将
cursor
设置为上一个响应中的next
- 将
previous
设置为上一个响应中的current
limit
是可选的 * 您可以将其设置为上一个请求中的count
以保持相同的限制
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
}
}
}