diff --git a/docs/docs/framework_base_docker-dev.md b/docs/docs/framework_base_docker-dev.md new file mode 100644 index 0000000..269b8cc --- /dev/null +++ b/docs/docs/framework_base_docker-dev.md @@ -0,0 +1,15 @@ +--- +url: framework_base_docker-dev +--- + +# docker环境 + +ECShopX 采用前后端分离的方式开发,代码结构由后端api和多个前端组成。 + +后端api由lumen开发,部署时包含了web、scheduler、worker和websocket。 + +前端包含管理端、小程序、PC端和H5端。 + +本环境目前只包含了web和管理的部署,后续会根据需求将其他环境加入。 + +详细使用方式[在此查看](https://github.com/ShopeX/ecshopx-open-docker) diff --git a/docs/docs/framework_bundle_controller.md b/docs/docs/framework_bundle_controller.md new file mode 100644 index 0000000..feeb71c --- /dev/null +++ b/docs/docs/framework_bundle_controller.md @@ -0,0 +1,243 @@ +--- +url: framework_bundle_controller +--- + +# 控制器 + +控制器都放在 Http 目录,与 [路由](framework_bundle_route) 相对应,控制器也按照终端进行了分组: + +- Admin +- Api +- Frontapi +- Shopapi +- Super +- Thirdparty + +每个分组下的目录结构如下: + +- Api + + - V1 + + - Action   控制器实际所在目录 + - Swagger  Swagger定义目录 + + +## 定义控制器 + +下面是一个基础控制器类的例子。需要注意的是,该控制器继承了 Laravel 的基类控制器。该基类控制器提供了一些便利的方法,比如 middleware 方法,该方法可以为控制器行为添加中间件: + +``` +namespace AftersalesBundle\Http\Api\V1\Action; + +use EspierBundle\Jobs\ExportFileJob; +use Illuminate\Http\Request; +use App\Http\Controllers\Controller as Controller; + +use AftersalesBundle\Services\AftersalesService; + +use Dingo\Api\Exception\ResourceException; + +use EspierBundle\Traits\GetExportServiceTraits; + +class Aftersales extends Controller +{ + use GetExportServiceTraits; + + public function getAftersalesDetail($aftersales_bn) + { + $companyId = app('auth')->user()->get('company_id'); + $aftersalesService = new AftersalesService(); + $result = $aftersalesService->getAftersalesInfo($companyId, $aftersales_bn); + + return $this->response->array($result); + } +} +``` + + +## 定义 API 文档 + +在 ECShopX 中每一个控制器方法都是一个 api都是一个接口,接口的出参和入参都是通过 注释来定义的,采用Swagger格式来定义。 + +控制器分组下的每一个版本都是一组 API ,定义一组 api 需要两步,以 AftersalesBundle 举例如下: + + +### 1、在 Swagger 目录下定义分组信息 + +定义 API 基本信息 + +```php +//AftersalesBundle/Http/Api/V1/Swagger/Info.php + +### 2、在 Action 目录中的控制器的方法定义接口 + +```php +/** + * @SWG\Get( + * path="/aftersales/{$aftersales_bn}", + * summary="获取售后单详情", + * tags={"aftersales"}, + * description="获取售后单详情", + * operationId="getAftersalesDetail", + * @SWG\Parameter( + * name="Authorization", + * in="header", + * description="JWT验证token", + * required=true, + * type="string", + * ), + * @SWG\Parameter( + * name="aftersales_bn", + * in="path", + * description="售后单号", + * required=true, + * type="integer", + * ), + * @SWG\Response( + * response=200, + * description="成功返回结构", + * ), + * @SWG\Response( response="default", description="错误返回结构", @SWG\Schema( type="array", @SWG\Items(ref="#/definitions/AftersalesErrorRespones") ) ) + * ) + */ + public function getAftersalesDetail($aftersales_bn) + { + $companyId = app('auth')->user()->get('company_id'); + $aftersalesService = new AftersalesService(); + $result = $aftersalesService->getAftersalesInfo($companyId, $aftersales_bn); + + return $this->response->array($result); + } +``` + +Swagger 定义参数详见  [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#dataTypeType) + + +## 接口文档生成和接口测试 + +在通过 Swagger 注释的方式定义好接口文档后,可以通过本地开发环境查看和测试接口。 + + +### 一、将 Swagger UI的资源拷贝到public目录中 + +执行以下命令: + +```shell +php artisan api:swagger --setup +``` + + +### 二、生成指定目录的Swagger API Josn + +以 AftersalesBundle 举例如下: + +```shell +php artisan api:swagger --output=src/AftersalesBundle/Http/Api/V1 +``` + +执行完命令后,默认会在 `storage/app/apidocs` 目录下生成类似 `售后单商家端调用接口[1.0].json`文件。 + +可通过在 `.env` 中新增配置改变存储目录名称 + +```shell +SWAGGER_STORAGE_DIR=apidocs +``` + + +### 三、启动服务查看和测试接口 + +可以通过 PHP 内置 server 快速启动 + +```shell +php -S 0.0.0.0:8005 -t public/ +``` + +访问地址为:[http://127.0.0.1:8085/api-doc](http://127.0.0.1:8085/api-doc)
+本机开发时,还需要将测试接口的地址修改为本地环境: + +``` +SWAGGER_API_HOST=127.0.0.1:8080 +SWAGGER_API_BASE_PATH=/api +``` + +使用技巧:
+在开发过程中可以,每次修改接口文档都需要重新生成接口文档,可以这样使用命令: + +``` +php artisan api:swagger --output=src/AftersalesBundle/Http/Api/V1 && php -S 0.0.0.0:8005 -t public/ +``` diff --git a/docs/docs/framework_bundle_database-base.md b/docs/docs/framework_bundle_database-base.md new file mode 100644 index 0000000..f1c18fe --- /dev/null +++ b/docs/docs/framework_bundle_database-base.md @@ -0,0 +1,397 @@ +--- +url: framework_bundle_database-base +--- + +# 快速入门 + +对于任何应用程序来说,一个最常见和最具挑战的任务,就是从数据库中读取和持久化数据信息。在 ECShopx 中我们采用 Doctrine ORM 替代 Laravel 自带的 Eloquent ORM ,前者更灵活,且自带 [Repository 模式](framework_architecture-repository)。本节将为大家介绍如何使用 Doctrine ORM。 + + +## 几个概念 + + +### 什么是 Doctrine ORM ? + +Doctrine ORM 是 PHP 7.1+ 的对象关系映射器(ORM),可以将PHP对象持久化到数据库中。它以Data Mapper模式为核心,旨在将您的业务逻辑与数据库中的持久化完全分离。 + + +### 什么是 Entities ? + +实体是简单的PHP对象,其含可持久化的属性,可持久属性是实体的一个实例变量,通过Doctrine的数据映射功能可以将其保存到数据库中或从数据库中检索出来。实体类不需要扩展任何抽象基类或接口。 + +实体类不能为final,尽管它可以包含final方法。 + + +### 什么是 DQL ? + +DQL 是 Doctrine Query Language 的缩写,代表 Doctrine 查询语言,并且是对象查询语言(Object Query Language)的派生类,它与 Hibernate 查询语言(HQL)或Java持久化查询语言(JPQL)非常相似。 + +::: info +对于初学者来说,常见的错误是将DQL误认为只是某种形式的SQL,因此尝试在查询中使用表名和列名或将任意表连接在一起。您需要考虑DQL作为对象模型而不是关系模式的查询语言。 +::: + + +## 定义数据库 Schema + +在 ECOS 中可以在 dbschema 文件夹下,使用数组定义数据库 Schema,laravel 中可以书写数据库迁移(database migration)文件,而在 ECShopX 中,则需要定义 Doctrine ORM 的实体类来完成数据库 Schema定义。 + +我们从最简单的实体 `Product` 开始。创建一个 `src/DemoBundle/Entities/Product.php`包含 Product 实体定义的文件: + +```php + +这个类仅仅是一个普通的 php 类,还不能称之为 Entity 类。我们需要通过为这个类添加注解使之成为 Entity 类。 + +```php +id; + } + + /** + * Set name. + * + * @param string $name + * + * @return Product + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name. + * + * @return string + */ + public function getName() + { + return $this->name; + } +} +``` + + +## 生成数据表 + +一个实体类定义了一张数据表,如何将数据表同步到数据库呢?
+首先需要先运行: + +```shell +php artisan doctrine:migrations:diff +``` + +运行以上命令成功后,会生成数据库迁移文件: + +``` +Generated new migration class to "Version20191225161708" from schema differences. +``` + +文件的路径在`database/migrations/Version20191225161708.php`
+生成之后,即可运行一下命令,将数据表写入到数据库: + +``` + php artisan doctrine:migrations:migrate +``` + + +## 持久化对象到数据库 + +现在我们有了Product实体和与之映射的product数据库表。接下里我们演示如何把数据持久化到数据库里。
+为了方便演示,我们采用测试用例的方式来演示此功能: + +```php +//src/DemoBundle/Tests/ProductTest.php +setName("EcshopX"); + + //这一行取出了Doctrine的 entity manager 对象,它负责处理数据库的持久化(写入)和取出对象的过程。 + $em = app('registry')->getManager('default'); + + // 告诉Doctrine你希望(最终)存储Product对象(还没有语句执行) + $em->persist($product); + + // 真正执行语句(如,INSERT 查询) + $em->flush(); + + $this->assertNotNull($product->getId()); + } +} +``` + +当 flush() 方法被调用时,Doctrine会遍历它管理的所有对象以确定是否需要被持久化到数据库。本例中, $product 对象的数据在库中并不存在,因此entity manager要执行 INSERT 请求,在 product 表中创建一个新行。 + + +## 从数据库中获取对象 + +从数据库中取回对象就更简单了: + +```php +//src/DemoBundle/Tests/ProductTest.php +public function testGetProduct() +{ + $productId = 1; + //这一行取出了Doctrine的 entity manager 对象,它负责处理数据库的持久化(写入)和取出对象的过程。 + $em = app('registry')->getManager('default'); + + $productRepository = $em->getRepository(Product::class); + + $product = $productRepository->find($productId); + + + $this->assertEquals($productId, $product->getId()); +} +``` + +当你要查询某个特定类型的对象时,你总是要使用它的”respository”。你可以认为Respository是一个PHP类,它的唯一工作就是帮助你从那个特定的类中取出entity。对于一个entity类,要访问其宝库,通过: + +```php +app('registry')->getManager('default')->getRepository(Product::class); +``` + +一旦有了Repository对象,你就可以访问它的全部有用的方法了。 + +```php +$repository = app('registry')->getManager('default')->getRepository(Product::class); + // 通过主键(通常是id)查询一件产品 +$product = $repository->find($productId); + +// 动态方法名称,基于字段的值来找到一件产品 +$product = $repository->findOneById($productId); +$product = $repository->findOneByName('Keyboard'); + +// 动态方法名称,基于字段值来找出一组产品 +$products = $repository->findByPrice(19.99); + +// find *all* products / 查出 *全部* 产品 +$products = $repository->findAll(); +``` + +你也可以有效利用 `findBy` 和 `findOneBy` 方法,基于多个条件来轻松获取对象: + +```php +$repository = app('registry')->getManager('default')->getRepository(Product::class); + +// 查询一件产品,要匹配给定的名称和价格 +$product = $repository->findOneBy( + array('name' => 'Keyboard', 'price' => 19.99) +); + +// 查询多件产品,要匹配给定的名称和价格 +$products = $repository->findBy( + array('name' => 'Keyboard'), + array('price' => 'ASC') +); +``` + + +## 对象更新 + +一旦从Doctrine中获取了一个对象,更新它就很容易了: + +```php +public function testUpdateProduct() +{ + $productId = 1; + //这一行取出了Doctrine的 entity manager 对象,它负责处理数据库的持久化(写入)和取出对象的过程。 + $em = app('registry')->getManager('default'); + $productRepository = $em->getRepository(Product::class); + $product = $productRepository->find($productId); + + $newName = "ECShopX 2.0"; + + $product->setName($newName); + $em->flush(); + + //判断数据是否更新成功 + $newProduct = $productRepository->find($productId); + $this->assertEquals($newName, $newProduct->getName()); +} +``` + +更新一个对象包括三步: + +- 1、从Doctrine中取出对象; +- 2、修改对象; +- 3、调用entity manager的 flush() 方法。 + +注意调用 $em->persist($product) 是不必要的。回想一下,这个方法只是告诉Doctrine去管理或者“观察” $product 对象。此处,因为你已经取到了 $product 对象了,它已经被管理了。 + + +## 删除对象 + +删除一个对象十分类似,但需要从entity manager调用 remove() 方法: + +```php +$em->remove($product); +$em->flush(); +``` + +你可能已经预期,`remove()` 方法通知Doctrine你想从数据库中删除指定的entity。真正的 DELETE 查询不会被真正执行,直到 `flush()` 方法被调用。 + + +## 对象查询 + +你已经看到 repository 对象是如何让你执行一些基本查询而毋须做任何工作了: + +```php +$repository = app('registry')->getManager('default')->getRepository(Product::class);; + +$product = $repository->find($productId); +$product = $repository->findOneByName('Keyboard'); +``` + +当然,Doctrine 也允许你使用Doctrine Query Language(DQL)来写一些复杂的查询,DQL类似于SQL,只是它用于查询一个或者多个entity类的对象(如 product),而SQL则是查询一个数据表中的行(如 product )。 + +在Doctrine中查询时,你有两个主要选择: + +- 编写纯正的Doctrine查询(DQL) +- 使用Doctrine的Query Builder + + +## 使用DQL进行对象查询 + +假设你要查询价格高于 19.99 的产品,并且按价格从低到高排列。你可以使用DQL,Doctrine中类似原生SQL的语法,来构造一个用于此场景的查询: + +```php +$em = $this->getDoctrine()->getManager(); +$query = $em->createQuery( + 'SELECT p + FROM DemoBundle\Entities\Product p + WHERE p.price > :price + ORDER BY p.price ASC' +)->setParameter('price', 19.99); + +$products = $query->getResult(); +``` + +如果你习惯了写SQL,那么对于DQL也会非常自然。它们之间最大的不同就是你需要就“select PHP对象”来进行思考,而不是数据表的行。正因为如此,你要 从 Product 这个 entity 来select,然后给entity一个 p 的别名。 + +> 注意 `setParameter()`` 方法。当使用 Doctrine 时,通过“占位符”来设置任意的外部值(上面例子的 :price),是一个好办法,因为它可以防止SQL注入攻击。 + + +getResult() 方法返回一个结果数组。要得到一个结果,可以使用getSingleResult()(这个方法在没有结果时会抛出一个异常)或者 getOneOrNullResult() : + +```php +$product = $query->setMaxResults(1)->getOneOrNullResult(); +``` + +DQL语法强大到令人难以置信,允许轻松地在entity之间进行join(稍后会覆盖relations)和group等。参考 [Doctrine Query Language](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html) 文档以了解更多。 + + +## 使用Doctrine's Query Builder进行对象查询 + +除了去写DQL,你还可以使用一个非常有用的QueryBuilder对象,来构建查询 SQL。当你的查询取决于动态条件时,这很有用,因为随着你的连接字符串不断增加,DQL代码会越来越难以阅读: + +```php + + +$em = app('registry')->getManager('default'); +$productRepository = $em->getRepository(Product::class); + +// createQueryBuilder() 自动从 AppBundle:Product 进行 select 并赋予 p 假名 +$query = $productRepository->createQueryBuilder('p') + ->where('p.price > :price') + ->setParameter('price', '19.99') + ->orderBy('p.price', 'ASC') + ->getQuery(); + +$products = $query->getResult(); +// 要得到一个结果: +$product = $query->setMaxResults(1)->getOneOrNullResult(); +``` + +QueryBuilder对象包含了创建查询时的所有必要方法。通过调用getQuery()方法,query builder将返回一个标准的Query对象,可用于取得请求的结果集。 + +Query Builder更多信息,参考 Doctrine 的 [Query Builder](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html)文档。 diff --git a/docs/docs/framework_bundle_database-crud-repository.md b/docs/docs/framework_bundle_database-crud-repository.md new file mode 100644 index 0000000..5d16b02 --- /dev/null +++ b/docs/docs/framework_bundle_database-crud-repository.md @@ -0,0 +1,182 @@ +--- +url: framework_bundle_database-crud-repository +--- + +# CRUD Repository + +为提高开发速度,降低代码重复率,新增 2 个 CURD Trait :`DBALCrudRepository` 和 `ORMCrudRepository`。 + +在 Doctrine ORM 中 ORM 依赖于 DBAL 层。ORM 调用 DBAL 并将 DBAL 返回的数组结构转化为对象。由于多了这一层转化,开发人员在使用时,可以根据使用场景和项目规模决定使用哪个Trait。 + +两者都是基于 Doctrine 提供的 `QueryBuilder`实现,不同的是: + +- `DBALCrudRepository` 基于 SQL Query Builder (Doctrine\DBAL\Query\QueryBuilder) +- `ORMCrudRepository` 基于 DQL Query Builder(Doctrine\ORM\QueryBuilder) + + +## DBALCrudRepository + +`DBALCrudRepository` 基于 SQL Query Builder (Doctrine\DBAL\Query\QueryBuilder),其结果返回为数组,性能比ORMCrudRepository 略高。 + +```php +trait DBALCrudRepository{ + public function save(array &$data) + public function create(array &$data) { + public function batchUpdate(array $filter, array $data) + public function batchDelete(array $filter) + public function count(array $filter=[]) + public function getList($cols='*', array $filter=[], $page = 1, $pageSize = 100, $orderBy = []) +} +``` + +使用方法如下: + +```php + +## ORMCrudRepository + +`ORMCrudRepository` 基于 DQL Query Builder(Doctrine\ORM\QueryBuilder) + +```php +public function save($entity) +public function delete($entity) +public function create(array $data) { +public function batchUpdate(array $filter, array $data) +public function batchDelete(array $filter) +public function count(array $filter=[]) +public function getList(array $filter=[], $page = 1, $pageSize = 100, $orderBy = []) +``` + +ORMCrudRepository 与 DBALCrudRepository 不同是 save 和 delete 方法,其输入为实体类,而不是数组。 + +使用方法如下: + +```php + +## DoctrineArrayFilter + +为了兼容现有代码和大家在 ECOS 中的使用习惯,在batchUpdate、batchDelete、count和getList中的 filter数组统一由 DoctrineArrayFilter 来解析。除支持 `字段|操作符` 外,还支持多层嵌套的 OR 和 AND 结构。 + + +### 支持结构 + + +#### and + +```php +$filter = [ + 'name|eq'=>'100', + 'name|neq'=>'100', + 'name|lt'=>'100', + 'name|lte'=>'100', + 'name|gt'=>'100', + 'name|gte'=>'100', + 'name|isNull'=>'', + 'name|isNotNull'=>'', + 'name|like'=>'%bar%', + 'name|notLike'=>'%foo', + 'name|in'=>[100,1000], + 'name|notIn'=>[1100,200], +]; +``` + +生成where 条件如下: + +``` +(name = '100') AND (name <> '100') AND (name < '100') AND (name <= '100') AND (name > '100') AND (name >= '100') AND (name IS NULL) AND (name IS NOT NULL) AND (name LIKE '%bar%') AND (name NOT LIKE '%foo') AND (name IN ('100', '1000')) AND (name NOT IN ('1100', '200')) +``` + + +#### OR + +```php + $filter = [ + 'OR'=>[ + 'name_or_or'=>1000, + 'name_or_or2'=>1000, + ], + ]; +``` + +生成where 条件如下: + +``` +(name_or_or = '1000') OR (name_or_or2 = '1000') +``` + + +#### 多层嵌套 + +```php +$filter1 = [ + 'AND'=>[ + 'name_and1|like'=>'zhang', + 'name_and2|isNull'=>'', + 'OR'=>[ + 'name_or1|in'=>[100,2020], + 'name_or2|notIn'=>[100,2020], + 'AND'=>[ + 'name_or_or'=>1000, + 'name_or_or2'=>1000, + ], + ], + ], + 'OR'=>[ + 'name_or1|in'=>[100,2020], + 'name_or2|notIn'=>[100,2020], + 'AND'=>[ + 'name_or_or'=>1000, + 'name_or_or2'=>1000, + ], + ], +]; +``` + +生成where 条件如下: + +```sql + ((name_and1 LIKE 'zhang') AND (name_and2 IS NULL) AND ((name_or1 IN ('100', '2020')) OR (name_or2 NOT IN ('100', '2020')) OR ((name_or_or = '1000') AND (name_or_or2 = '1000')))) AND ((name_or1 IN ('100', '2020')) OR (name_or2 NOT IN ('100', '2020')) OR ((name_or_or = '1000') AND (name_or_or2 = '1000'))) +``` + + +### 支持操作符 +| 操作符 | 数据库含义 | +| :---: | :---: | +| eq | = | +| neq | <> | +| lt | < | +| lte | <= | +| gt | > | +| gte | >= | +| in | in | +| notIn | not in | +| isNull | name IS NULL | +| isNotNull | name IS NOT NULL | +| like | LIKE | +| notLike | NOT LIKE | + diff --git a/docs/docs/framework_bundle_database-orm.md b/docs/docs/framework_bundle_database-orm.md new file mode 100644 index 0000000..940d8c3 --- /dev/null +++ b/docs/docs/framework_bundle_database-orm.md @@ -0,0 +1,29 @@ +--- +url: framework_bundle_database-orm +--- + +# Doctrine ORM + + +## 什么是 ORM ? + +ORM 是 Object-Relation Mapping (对象-关系映射)的简称,是随着面向对象的软件开发方法发展而产生的。
+Doctrine ORM 是 PHP 的对象关系映射器(ORM),可以将PHP对象持久化到数据库中。它以Data Mapper模式为核心,旨在将您的业务逻辑与数据库中的持久化完全分离。 + +ORM 主要做了两件事: + +- ORM 使用对象,封装了数据库操作,因此可以不碰 SQL 语言就可以从数据库获取数据 +- ORM 将数据库映射为对象,或将对象持久化到数据库 + +ORM 把数据库映射成对象关系如下: + +- 数据库的表(table) --> 类(class) +- 记录(record,行数据)--> 对象(object) +- 字段(field)--> 对象的属性(attribute) + + +## 如何使用 Doctrine ORM ? + +PHP 数据对象(PDO)扩展为PHP访问数据库定义了一个轻量级的一致接口,通过此扩展从数据库获取数据其结果集为一个数组。 + +在 ECOS 中,我们也对数据访问做了封装,可以通过 model 去获取数据,其结果也是也是一个数组,程序员通过 model 获取一个结果后,即可针对这个结果的数组做相关的业务处理。 diff --git a/docs/docs/framework_bundle_database-relations.md b/docs/docs/framework_bundle_database-relations.md new file mode 100644 index 0000000..0a0ea89 --- /dev/null +++ b/docs/docs/framework_bundle_database-relations.md @@ -0,0 +1,6 @@ +--- +url: framework_bundle_database-relations +--- + +# 数据库和 Doctrine ORM + diff --git a/docs/docs/framework_bundle_database-repository.md b/docs/docs/framework_bundle_database-repository.md new file mode 100644 index 0000000..f0561ee --- /dev/null +++ b/docs/docs/framework_bundle_database-repository.md @@ -0,0 +1,136 @@ +--- +url: framework_bundle_database-repository +--- + +# Repository + +在上一章,所有的查询是直接写在你的测试用例的方法中的。但对于程序的组织来说,为了隔离,复用和测试这些查询 Doctrine 提供了一个专门的 repository 类,它允许你保存所有查询逻辑到一个中心位置。 + +所有的 repository 类,都应该放到 Bundle 的 Repositories 文件夹中。 + +要定义repository类,有两种方法。 + + +## 一、在实体类中添加 repository 类的声明: + +```php +// src/DemoBundle/Entities/Product.php +getEntityManager() + ->createQuery( + 'SELECT p FROM DemoBundle\Entities\Product p ORDER BY p.name ASC' + ) + ->getResult(); + } +} +``` + +> 在 Repository 类中可以通过 $this->getEntityManager() 方法类获取entity管理。 + + +你就可以像使用默认的方法一样使用这个新定义的方法了: + +```php +//src/DemoBundle/Tests/ProductTest.php +/** + * A basic test example. + * + * @return void + */ +public function testProductRepository() +{ + //这一行取出了Doctrine的 entity manager 对象,它负责处理数据库的持久化(写入)和取出对象的过程。 + $em = app('registry')->getManager('default'); + + $productRepository = $em->getRepository(Product::class); + + $products = $productRepository->findAllOrderedByName(); + + $this->assertEquals(count($productRepository->findAll()), count($products)); +} +``` + +> 当使用一个自定义的 repository 类时,你依然可以访问原有的默认查找方法,比如find() 和findAll()等。 + + +使用注解的方式 Doctrine 会在调用 `$em->getRepository(Product::class);` 时,通过实体的注解找到实体对应的 repository 类。这种方式是 Doctrine 默认提供的,其有一个确定就是:这样定义一个实体只能有一个 repository ,将程序所有自定义查询都放到一个类里面,随着项目的发展,势必会导致 repository 类的肥大。这显然违反 SOLID 的单一职责原则。 + + +## 二、通过在 repository 类中声明关联的实体 + +与第一种方法相反,这种方式是通过在 repository 类中声明所关联的实体,来实现 repository 类的声明。由于 repository 类初始化时,需要传入Entity Manager 和实体类的 ClassMate,为了方便初始化 repository 类,需要引入一个 Traits 。 + +在 Repositories 文件夹下新建一个 ProductPriceRepository 类,用来处理价格相关的查询 : + +```php +//src/DemoBundle/Repositories/ProductPriceRepository.php +getEntityManager() + ->createQuery( + 'SELECT p FROM DemoBundle\Entities\Product p ORDER BY p.name ASC' + ) + ->getResult(); + } +} +``` + +使用方式如下: + +```php +//src/DemoBundle/Tests/ProductTest.php + +use DemoBundle\Repositories\ProductPriceRepository; + + //... + + public function testProductPriceRepository() + { + $productRepository = ProductPriceRepository::instance(); + + $products = $productRepository->findAllOrderedByName(); + + $this->assertEquals(count($productRepository->findAll()), count($products)); + } +``` diff --git a/docs/docs/framework_bundle_intro.md b/docs/docs/framework_bundle_intro.md new file mode 100644 index 0000000..01c0583 --- /dev/null +++ b/docs/docs/framework_bundle_intro.md @@ -0,0 +1,35 @@ +--- +url: framework_bundle_intro +--- + +# Bundle + +Laravel(Lumen)官方建议将所有代码都放到 app 目录下面: + + +> - app 目录包含应用程序的核心代码。你应用中几乎所有的类都应该放在这里。 +> - app 目录包含额外的各种目录,比如:Console, Http, 和 Providers + + + +按照官方的建议将所有的应用程序代码都放到 app 目录里面,随着项目的增大,代码增多,需求逐渐复杂,后期将会导致代码过于复杂,维护成本急剧增高。 + +借鉴商派 ECOS 框架 APP 机制和 symfony 的 Bundle 系统,我们在 ECShopX 中也实现了一种模块化的机制,我们也称为 Bundle。 + +一个 Bundle 就是一个模块,其包含模块所有的资源。所有的 Bundle 都位于 `src` 目录下,
+Bundle 的命名规则为`业务名称+Bundle`, 它的目录结构如下: + +| 文件/目录 | 描述 | +| --- | --- | +| **Entities** | 包含本 Bundle 的所有实体类,一个实体类对应一张数据表。可以通过修改实体类来维护表结构 | +| **Events** | 包含事件类 | +| **Http** | 包含控制器,按照Api、FrontApi、ShopApi等分组,每个控制器都有所属版本号 | +| **Interfaces** | 包含本 Bundle 所有接口抽象 | +| **Jobs** | 包含队列任务类 | +| **Listeners** | 目录包含事件的处理类 | +| **Providers** | 包含本 Bundle 的服务提供者 | +| **Repositories** | 处理数据库逻辑,一般和entity表文件一对一。 | +| **Services** | 所有业务处理的逻辑放在此目录,供控制器调用 | +| **Traits** | 本 Bundle 可被复用的逻辑可以抽象为trait放在此目录 | +| **README.md** | 本 Bundle 的说明文件,介绍该 Bundle 的基本功能,主要类和一些设计思想,供其他开发人员参阅 | + diff --git a/docs/docs/framework_bundle_route.md b/docs/docs/framework_bundle_route.md new file mode 100644 index 0000000..ff60c78 --- /dev/null +++ b/docs/docs/framework_bundle_route.md @@ -0,0 +1,77 @@ +--- +url: framework_bundle_route +--- + +# 路由 + +ECShopX 采用 Dingo API 来构建和管理 API。 + +为了避免与项目路由冲突,dingo/api 将会使用其专属的路由实例。
+在 `bootstrap/route.php` 中定义了API 路由的实例: + +```php +$api = app('Dingo\Api\Routing\Router'); +``` + + +## 路由定义 + +ECShopX 前端包含了 admin管理端、shop 管理端、小程序、PC 和 H5 等终端,所以在`routes` 文件下,我们对路由做了分组: + +- admin +- api +- frontapi +- shopapi +- super +- thirdparty + +每个 Bundle 的路由根据需求定义在`routes`文件夹下的分组中,路由文件名称为业务名称,路由定义是必须定义版本号,默认为 `V1`,定义了一条路由就定义了一个接口。 + +路由定义的语法请参考 Dingo API,以 `AftersalesBundle` 为例: + +定义路由 + +```php +$api->version('v1', function($api) { + // 售后相关api + $api->group(['namespace' => 'AftersalesBundle\Http\Api\V1\Action', 'middleware' => ['api.auth', 'activated', 'shoplog'], 'providers' => 'jwt'], function($api) { + $api->get('/aftersales', ['name' => '获取售后列表', 'as' => 'aftersales.list', 'uses' => 'Aftersales@getAftersalesList']); + }); + +}); +``` + +其中 + +``` +'uses' => 'Aftersales@getAftersalesList' +``` + +`Aftersales`为对用控制器类名,`getAftersalesList`为方法名。 + +`AftersalesBundle`的路由会存在于以下几个分组: + +- routes/api/aftersales.php +- routes/frontapi/aftersales.php +- routes/shopapi/aftersales.php + + +## 路由注册 + +在定义路由之后,必须手工注册到`bootstrap/route.php`中,才可以使用。 + +比如 `AftersalesBundle` 新增了一组 admin 的路由,我们定义文件路径为: + +``` +routes/admin/aftersales.php +``` + +我们需要将此文件加入到`bootstrap/route.php`中: + +``` + + +**使用前请确保您已安装PHP 和 Composer**
+ + + +## 使用国内源 + +- aliyun [https://mirrors.aliyun.com/composer/](https://mirrors.aliyun.com/composer/) 推荐 +- tencent [https://mirrors.cloud.tencent.com/composer/](https://mirrors.cloud.tencent.com/composer/) + + + +### 方法1:修改全局配置 + +打开终端并执行如下命令: + +``` +composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ +``` + +打开终端并执行如下命令: + +``` +composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ +``` + + +### 方法2:修改项目配置 + +打开终端,进入你的项目的根目录(也就是 composer.json 文件所在目录),执行如下命令: + +``` +composer config repo.packagist composer https://mirrors.aliyun.com/composer/ +``` + +您也可以手工在composer.json中添加如下内容: + +"repositories": {
+"packagist": {
+"type": "composer",
+"url": "[https://mirrors.aliyun.com/composer/](https://mirrors.aliyun.com/composer/)"
+}
+} + + +## 相关网址 + +- PHP官方地址:[http://php.net/](http://php.net/) +- Composer官方地址:[https://getcomposer.org/](https://getcomposer.org/) diff --git a/docs/docs/framework_debug.md b/docs/docs/framework_debug.md new file mode 100644 index 0000000..81d377b --- /dev/null +++ b/docs/docs/framework_debug.md @@ -0,0 +1,90 @@ +--- +url: framework_debug +--- + +# 调试 + +- 接口调试可通过POSTMAN来进行. +- 异常处理可依赖, 系统日志. +- 微信调试, 可以依赖微信日志. +- 正式环境强烈建议使用 _SENTRY_, 捕获错误并及时处理 + +![](../assets/c5be9fcffcb3f524bdbb4e969415662d.svg) + + +## 日志调试 + + +### 系统日志 + +- 系统日志 DEBUG/INFO/NOTICE/WARNING/ERROR/CRITICAL/EMERGENCY +- 数据库执行日志 + +```php +# 修改 .env +DOCTRINE_LOGGER=LaravelDoctrine\ORM\Loggers\FileLogger +``` + +系统日常默认放置在 _storage/logs/lumen.log_ + +可参考: [lumen Errors & Logging](https://lumen.laravel.com/docs/5.4/errors) + + +### 微信调试 + +- 微信开放平台第三方平台日志 +- 微信公众号日志 +- 微信小程序日志 + +日志默认放置在 _storage/logs/wechat.log_ + + +## 手动异常抛出 + +有的场景捕获异常后, 依然需要完整的_Exception_抛出, 以便问题排查, 这是可以通过_app('api.exception')->report($e)_进行抛错 + +例如: + +```php +report($e); + + $exceptionMessage = $e->getMessage(); + + #... +} +``` + + +## SENTRY异常捕获平台 + +可配置_.env_ + +```php +SENTRY_LARAVEL_DSN= +``` + +参考: + +- [SENTRY官方文档](https://sentry.io) +- [sentry使用实践](https://www.jianshu.com/p/66e00077fac3) + + +## 记录日志 + +```php +debug('debug'); +app('log')->info('info'); +app('log')->notice('notice'); +app('log')->warning('warning'); +app('log')->error('error'); +app('log')->crit('critical'); +app('log')->alert('alert'); +app('log')->emerg('emerg'); +``` diff --git a/docs/docs/framework_doc-readme.md b/docs/docs/framework_doc-readme.md new file mode 100644 index 0000000..bf7a4a2 --- /dev/null +++ b/docs/docs/framework_doc-readme.md @@ -0,0 +1,13 @@ +--- +url: framework_doc-readme +--- + +# 文档说明 + +ecshopX 的 API 层基于 Lumen 框架开发,而 Lumen 的 是基于 Laravel 开发,在 ECShopX 中我们根据商派在电商领域的多年经验对其做了一些调整,本文档主要对我们调整部分的说明,及其一些架构思想的阐述。其他文档请参阅 Lumen 和 Laravel 的文档。 + +附相关文档地址: + +- [Lumen 中文文档](https://learnku.com/docs/lumen/5.5) +- [Laravel 中文文档](https://learnku.com/docs/laravel/5.5) +- [Dingo 中文文档](https://learnku.com/docs/dingo-api/2.0.0) diff --git a/docs/docs/framework_event.md b/docs/docs/framework_event.md new file mode 100644 index 0000000..a4fe408 --- /dev/null +++ b/docs/docs/framework_event.md @@ -0,0 +1,237 @@ +--- +url: framework_event +--- + +# 事件系统 + + +## 事件系统介绍 + +Laravel 为事件提供了一个简单的观察者实现,允许你在应用中订阅和监听各种发生的事件。
+在 Laravel 中事件类通常放在 app/Events 目录下,这些事件类的监听器则放在 app/Listeners 目录下。而在 ECShopx 中事件和监听器则放在 Bundle 对应的目录中。 + +事件系统为应用各个方面的解耦提供了非常棒的方法,因为单个事件可以拥有多个互不依赖的监听器。 + +举个例子,在电商系统中,订单完成时,可能希望向用户发送短信通知或者微信通知,同时还需要处理一些订单相关的逻辑。你可以简单地发起一个可以被监听器接收并转化为短信通知的 TradeFinishEvent 事件,而不是将订单处理代码和发送短信通知代码耦合在一起。 + + +## 定义事件 + +事件类是一个保存与事件相关信息的容器。它保存在 Bundle 的 Events 目录中。例如,假设我们定义一个 TradeFinishEvent 事件,其接收一个 Trade Entity 对象: + +```php +entities = $eventData; + } +} +``` + +如你所见,这个事件类中没有包含其它逻辑。它只是一个 Trade 的实例的容器。 + + +## 定义监听器 + +事件监听器是一个特殊的 PHP 类,其包含了一个接受事件的 handle 方法,handle 方法中加入事件的类型提示。它保存在 Bundle 的 Listeners 目录中。例如,在订单完成时,我们会给用户发送一个微信的模板消息。 + +```php + +## 注册事件和监听器 + +现在我们定义了一个订单完成的事件 `TradeFinishEvent` 和 一个订单完成时的监听器 `TradeFinishWxaTemplateMsg`,现在我们需要注册事件和监听器。 + +在 Laravel 中事件和监听器注册是在 `EventServiceProvider` 类中完成的,在 ECShopX 中,我们可以把它们放在每个 Bundle 的 Provider 中。 Provider 放在 Providers 目录下。 + +```php + [ + 'OrdersBundle\Listeners\TradeFinishWxaTemplateMsg', + ], + ]; +} +``` + +其中, listen 属性包含了所有事件 (键) 以及事件对应的监听器 (值) 的数组。当然,你可以根据应用的需要,添加多个事件到 listen 属性包含的数组中。 + +在注册完成之后,还需要将 TradeFinishServiceProvider 在 `bootstrap/app.php` 中: + +```php +$app->register(OrdersBundle\Providers\TradeFinishServiceProvider::class); +``` + + +## 分发事件 + +如果要分发事件,你可以将事件实例传递给辅助函数 event。该辅助函数将会把事件分发到所有该事件相应的已经注册了的监听器上。event 辅助函数可以全局使用,你可以在应用中的任何位置进行调用: + +```php +event(new TradeFinishEvent($eventsParams)); +``` + + +## 事件监听器队列(异步事件) + +如果你的监听器中要执行诸如发送电子邮件或发出 HTTP 请求之类的耗时任务,你可以将任务丢给队列处理。在开始使用队列监听器之前,请确保在你的服务器或者本地开发环境中能够 配置队列并启动一个队列监听器。 + +要指定监听器启动队列,你可以在监听器类中实现 ShouldQueue 接口。同时需要继承 `EspierBundle\Listeners\BaseListeners` 类: + +```php + +### 自定义队列连接 & 队列名称 + +如果你想要自定义事件监听器所使用的队列的连接和名称,你可以在监听器类中定义 $connection, $queue 或 $delay 属性: + +```php + +### 处理失败任务 + +有时事件监听器的队列任务可能会失败。如果监听器的队列任务超过了队列中定义的最大尝试次数,则会在监听器上调用 failed 方法。 failed 方法接收事件实例和导致失败的异常作为参数: + +```php + +## 简介 + +ECShopX 的 API 层基于 Lumen 框架开发,根据商派在电商领域的多年经验对其做了一些调整,主要调整如下: + +- 目录结构调整:除基本配置目录外,lumen自带的app目录基本已经废弃,借鉴商派ecos及symfony思想,ECShopX引入了 Bundle,每个Bundle包含了一个独立的业务。 +- ORM调整:Lumen底层基于 Laravel 开发,所以 Luemn 底层ORM采用Laravel 的 Eloquent ORM实现来和数据库进行交互。考虑到更灵活的SQL能力,我们采用了Doctrine ORM。 + + +## 根目录 +| 文件/目录 | 描述 | +| --- | --- | +| **app** | 在 Lumen 中 app 目录包含了应用的核心代码,在ECShopX中不用关心此目录的内容 | +| **bootstrap** | 在 app.php 中注册 ServiceProvider ,在route.php中注册各个Bundle的路由文件 | +| **config** | config 目录包含了应用所有的配置文件,建议通读一遍这些配置文件以便熟悉所有配置项 | +| **database** | 包含了数据迁移及填充文件 | +| **public** | public 目录包含了入口文件 index.php | +| **routes** | 目录包含了 Bundle 的所有路由定义 | +| **src** | 存放 Bundle 的目录 | +| **storage** | storage 目录包含了编译过的Blade模板、基于文件的session、文件缓存,以及其它由框架生成的文件,该目录被细分为成app、framework和logs子目录 | +| **vendor** | vendor目录包含所有Composer依赖 | + diff --git a/docs/docs/framework_queue.md b/docs/docs/framework_queue.md new file mode 100644 index 0000000..6fffcdb --- /dev/null +++ b/docs/docs/framework_queue.md @@ -0,0 +1,186 @@ +--- +url: framework_queue +--- + +# 消息队列 + + +## 什么是消息队列? + +消息队列,一般我们会简称它为MQ(Message Queue),队列是一种先进先出的数据结构。可以简单理解为:把要传输的数据放在队列中。 + +目前使用较多的消息队列中间件有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。
+主要解决应用解耦,异步消息,流量削锋等问题。 + +消息队列中有四个重要的角色: + +- 消息:可被其他业务使用的数据 +- 队列:用于存储消息 +- 生产者:生产消息发送到消息队列中 +- 消费者:从消息队列中取消息 + +这些角色在不同的消息中间件中,有不同的实现方式,使用方式也不同,好在 Laravel 队列为不同的后台队列服务提供统一的 API,例如 RabbitMQ ,Amazon SQS,Redis,甚至其他基于关系型数据库的队列。 + + +## 配置 + +与队列相关的配置都放在 `config/queue.php` 中。 + +`connections` 这个选项给 RabbitMQ ,Beanstalk,或者 Redis 这样的后端服务定义了一个特有的`连接`。每个`连接`可以多多个 `队列`。 + +需要注意的是 `config/queue.php` 中,每个`连接`都包含了一个 `queue` 属性。队列任务被发给指定连接的时候会被分发到 `queue` 属性和指定连接相同的队列中。换句话说,如果你分发任务的时候没有定义分配到哪个队列,那么它就会被放到连接配置中 queue 属性所定义的默认队列中 + +`default` 选项定义了默认的是哪个连接。 + + +## 创建任务类 + +所有的任务类,都是放在每个 Bundle 的 Jobs 目录下。任务类需要继承 `Illuminate\Contracts\Queue\ShouldQueue` 接口,这意味着这个任务将会被推送到队列中,而不是同步执行。 + +任务类包含两个方法和一些属性,下面看下产品中的一个例子: + +```php +smsData = $smsData; + } + + public function handle() + { + $smsData = $this->smsData; + try { + $companyId = $smsData['company_id']; + $mobiles = $smsData['send_to_phones']; + $content = $smsData['sms_content']; + + app('log')->debug('短信群发1: fan-out =>'.$companyId); + $companysService = new CompanysService(); + $shopexUid = $companysService->getPassportUidByCompanyId($companyId); + + app('log')->debug('短信群发2: fan-out =>'.$shopexUid); + $smsService = new SmsService(new ShopexSmsClient($companyId, $shopexUid)); + + $smsService->sendContent($companyId, $mobiles, $content, 'fan-out'); + } catch ( \Exception $e) { + app('log')->debug('短信群发失败: fan-out =>'.var_export($e->getMessage(),1)); + } + } +} +``` + +- `__construct`: 构造方法用来初始化任务所需要的数据 +- `handle`:队列执行时所调用的方法 + +在队列处理任务时,会调用 `handle` 方法,而这里我们也可以通过 `handle` 方法的参数类型提示,让 `Laravel` 的 `服务容器` 自动注入依赖对象。 + +比如,上面例子中的 `handle` 方法中有这样一段代码 `$companysService = new CompanysService();` 我们可以这样改造: + +```php +public function handle(CompanysService $companysService) + { + $smsData = $this->smsData; + try { + $companyId = $smsData['company_id']; + $mobiles = $smsData['send_to_phones']; + $content = $smsData['sms_content']; + + app('log')->debug('短信群发1: fan-out =>'.$companyId); + $shopexUid = $companysService->getPassportUidByCompanyId($companyId); + + app('log')->debug('短信群发2: fan-out =>'.$shopexUid); + $smsService = new SmsService(new ShopexSmsClient($companyId, $shopexUid)); + + $smsService->sendContent($companyId, $mobiles, $content, 'fan-out'); + } catch ( \Exception $e) { + app('log')->debug('短信群发失败: fan-out =>'.var_export($e->getMessage(),1)); + } + } +``` + +这样写 `handle(CompanysService $companysService)` 在执行任务时 `Laravel` 会自动初始化 CompanysService 类,并注入 `handle` 中。 + + +## 分发任务 + +分发任务分为两步:先初始化任务类,然后再使用辅助方法dispatch()分发任务。 + +```php + public function smsSends(Request $request) + { + $inputdata = $request->all('mobile', 'sms_content'); + + $memberSmsLogService = new MemberSmsLogService(); + $params['company_id'] = app('auth')->user()->get('company_id'); + $params['operator'] = '管理员'; + $params['send_to_phones'] = $inputdata['mobile']; + $params['sms_content'] = $inputdata['sms_content']; + $result = $memberSmsLogService->create($params); + //分发队列 + $job = (new GroupSendSms($params))->onQueue('sms'); + dispatch($job); + + return $this->response->array($result); + } +``` + +dispatch() 辅助方法的具体实现如下: + +```php + function dispatch($job) + { + return app(Illuminate\Contracts\Bus\Dispatcher::class)->dispatch($job); + } +``` + + +### 同步调度 + +如果您想立即(同步)执行队列任务,可以使用 `dispatchNow` 方法。 使用此方法时,队列任务将不会排队,并立即在当前进程中运行: + +```php +app(Illuminate\Contracts\Bus\Dispatcher::class)->dispatchNow($job); +``` + + +## 运行队列处理器 + +创建完成任务类,dispatch任务后,任务会在`队列`中,要想真正执行任务还要执行`队列处理器`
+Laravel 包含了一个队列处理器以将推送到队列中的任务执行。你可以使用 queue:work Artisan 命令运行处理器。 注意一旦 queue:work 命令开始执行,它会一直运行直到它被手动停止或终端被关闭。 + +```shell +php artisan queue:work +``` + +> Tip:要使 queue:work 进程一直在后台运行,你应该使用进程管理器比如 Supervisor 来确保队列处理器不会停止运行 + + +记住,队列处理器是一个常驻的进程并且在内存中保存着已经启动的应用状态。因此,它们并不会在启动后注意到你代码的更改。所以,在你的重新部署过程中,请记得 重启你的队列处理器。 + + +## 思考 + +上面我们通过创建任务类,分发任务,运行队列处理器我们的任务就可以异步执行了。在文章开头我们提到的消息队列中有四个重要的角色:消息、队列、生产者、消费者,而在 `Laravel` 的封装下只有任务类、任务分发(dispatch)、队列处理器。它们之间的对应关系可以这样理解: + +- `消息` 是 初始化的任务类。 +- `队列` 是我们在`config/queue.php`中配置的`连接`所对应的`queue`,消息被分发后它们可能会保存在RabbitMQ、Redis或者数据库中的队列中。 +- `生产者` 是 `dispatch` +- `消费者`是`队列处理器`+`任务类的 handle 方法` + +一个消息从产出到消费的整个流程可以分三步理解: + +第一步:通过定义任务类的属性,定义消息结构体。同时在任务类中定义了消息的处理方法`handle`。 + +第二步:我们根据业务初始化任务类然后调用 `dispatch` 方法。`dispatch` 方法会根据规则将任务类格式化为字符串格,然后将字符串放入队列中,此时 `dispatch` 完成生产者的职责。 + +第三步:消息被投递到队列之后,`队列处理器`就会从队列中获取字符串消息,根据规则将字符串消息初始化为对应的任务类,然后再调用任务的 `handle` 方法,完成消息的消费。 diff --git a/docs/docs/framework_standard_base.md b/docs/docs/framework_standard_base.md new file mode 100644 index 0000000..d613b22 --- /dev/null +++ b/docs/docs/framework_standard_base.md @@ -0,0 +1,47 @@ +--- +url: framework_standard_base +--- + +# 说明 + + +## 说明 + +作为中国电子商务全面解决方案的先行者,商派技术团队通过十八年的实践,积累了大量的电子商务前后端的架构模式和中间件设计经验,正是通过这些积累,保证了商派在国内电商业界的领先地位。 + +从公司ShopEx 4.8系列产品开始,商派一直在探索一种行之有效的方式,既能够降低研发成本和研发工作的复杂性,又能够快速地跟进业务的发展变化,在这种方式下,可以省去很多基础性的研发工作,复用八年来获得的经验,同时使研发周期大幅缩短,提高研发效率。 + +自2009年立项开始,商派投入了30余名工程师,用将近一年的时间打造出了开源的电子商务业务架构平台,幵将其命名为ECOS,寓意电子商务操作系统。 + +在经过 10 年发展后,我们基于 ECOS 做出了大量电商产品,于此同时开源框架也发展迅速,在新零售来临时,我们决定采用开源框架来重构我们的产品,在重构过程中,我们对商派多年的开发经验和开源社区经验进行总结,形成了此开发开发规范。 + + +## 目的 + +规范有以下目的: + +- 高效编码 - 避免了过多的选择造成的『决策时间』浪费; +- 风格统一 - 最大程度统一了开发团队成员代码书写风格和思路,代码阅读起来如出一辙; +- 减少错误 - 减小初级工程师的犯错几率。 +- 代码可被生成 - 通过统一的规范,可以编写脚手架来生成重复性的代码。 + + +## 开发哲学 + +因为篇幅原因本规范无法涉及到项目里每一块代码的编写标准,所以此处重点说明下此规范遵循的『开发哲学』,开发中请把其当做指明灯,来指引你做决策: + +- DRY –「Don't Repeat Yourself」不写重复的逻辑代码; +- 约定俗成 - 「Convention Over Configuration」,优先选择框架提倡的做法,不过度配置; +- KISS - 「Keep it Simple, Stupid」提倡简单易读的代码,不写高深、晦涩难懂的代码,**不过度设计**; +- 主厨精选 - 让有经验的人来为你选择方案,不独创方案; +- 官方提倡 - 优先选择官方推崇的方案。 + + +## 设计理念 + +以下是一些优秀的『程序设计理念』: + +- MVC - Model, View, Controller ,以 MVC 为核心,新增 [Repository 模式](framework_standard_architecture-repository) 和 [Service 模式](framework_standard_architecture-service),来控制 Controller 和 Model 代码行数; +- Restful - 利用『资源化概念』和标准的 HTTP 动词来组织你的程序; + +在此规范中,我们会将使用这两套理念作为程序设计基础。这些设计理念为我们设计程序提供了依据,遵循这些理念,能让程序变得清晰易读。 diff --git a/docs/docs/framework_standard_basic-coding-standard.md b/docs/docs/framework_standard_basic-coding-standard.md new file mode 100644 index 0000000..65fda28 --- /dev/null +++ b/docs/docs/framework_standard_basic-coding-standard.md @@ -0,0 +1,155 @@ +--- +url: framework_standard_basic-coding-standard +--- + +# 基本代码规范 + +本篇规范制定了代码基本元素的相关标准,以确保共享的 PHP 代码间具有较高程度的技术互通性。
+
本文件中的 `必须`,`不得`,`需要`,`应`,`不应`,`应该`,`不应该`,`推荐`,`可能` 和 `可选` 等能愿动词按照 [RFC 2119](http://www.ietf.org/rfc/rfc2119.txt) 中的描述进行解释。
+ + +## 1. 概览 + + +- PHP代码文件 **必须** 以 ` +## 2. 文件 + + + +### 2.1. PHP 标签 + +
PHP 代码 **必须** 使用 `` 长标签 或 `` 短输出标签;
+**一定不可** 使用其它自定义标签。
+ + +### 2.2. 字符集编码 + +
PHP代码 **必须** 且只可使用 `不带 BOM 的 UTF-8` 编码。
+ + +### 2.3. 副作用 + +
一份 PHP 文件中 **应该** 要不就只定义新的声明,如类、函数或常量等不产生 `副作用` 的操作,要不就只书写会产生 `副作用` 的逻辑操作,但 **不该** 同时具有两者。
+
「副作用」(side effects) 一词的意思是,仅仅通过包含文件,不直接声明类、函数和常量等,而执行的逻辑操作。
+
「副作用」包含却不仅限于:生成输出,明确使用require或include,连接到外部服务,修改ini设置,发出错误或异常,修改全局或静态变量,读取或写入一个文件,等等。
+
以下是一个 `反例`,一份包含「函数声明」以及产生「副作用」的代码:
+ +```php +\n"; + +// 声明函数 +function foo() +{ + // function body +} +``` + +
下面是一个范例,一份只包含声明不产生「副作用」的代码:
+ +```php + +## 3. 命名空间和类名 + +
命名空间和类名 **必须** 遵循『自动加载』规范: [[PSR-0](https://learnku.com/docs/psr/psr-0-automatic-loading-specification), [PSR-4](https://learnku.com/docs/psr/psr-4-autoloader)]。
+
这意味着每个类都独立为一个文件,并且至少在一个层次的命名空间内,那就是:顶级组织名(vendor name)。
+
类名 **必须** 以类似 `StudlyCaps` 形式的大写开头的驼峰命名方式声明。
+
PHP 5.3 及更高版本的代码 **必须** 使用正式的命名空间。
+
举个例子:
+ +```php +PHP 5.2 及更低版本 **应该** 使用伪命名空间,约定俗成,以顶级组织名称 `Vendor_` 为类名前缀:
+ +```php + +## 4. 类的常量、属性和方法 + +
此处的「类」指代所有的类、接口以及可复用代码块(traits)。
+ + +### 4.1. 常量 + +
类的常量中所有字母都 **必须** 大写,词间以下划线分隔。例如:
+ +```php + +### 4.2. 属性 + +
类的属性命名 **可以** 遵循:
+ +- 大写开头的驼峰式 (`$StudlyCaps`) +- 小写开头的驼峰式 (`$camelCase`) +- 下划线分隔式 (`$under_score`) + + +
本规范不做强制要求,但无论遵循哪种命名方式,都 **应该** 在一定的范围内保持一致。这个范围可以是整个团队、整个包、整个类或整个方法。
+ + +### 4.3. 方法 + +
方法名称 **必须** 符合 `camelCase()` 式的小写开头驼峰命名规范。
+ +> 本文转自:[https://learnku.com/docs/psr/basic-coding-standard/1605](https://learnku.com/docs/psr/basic-coding-standard/1605) + diff --git a/docs/docs/framework_standard_bundle.md b/docs/docs/framework_standard_bundle.md new file mode 100644 index 0000000..d63a007 --- /dev/null +++ b/docs/docs/framework_standard_bundle.md @@ -0,0 +1,38 @@ +--- +url: framework_standard_bundle +--- + +# 命名规范 + + +## 命名规范 + +- Bundle 名 `必须` 为 `业务名称+Bundle` 整体命名遵循`大驼峰`规范。 +- Repository 类 `必须` 为 `业务名称+Repository` +- Service 类 `必须` 为 `业务名称+Service` +- Event 类 `必须` 为 `业务名称+Event` + + +## Entity 类 + +所有实体类 必须 放置在 `{Name}Bundle/Entities` 目录下。 + +实体类的所有属性 `必须` 为 `private`, `绝不` 使用 `public` + +实体类的所有属性 `必须` 有对应的 `getter` 方法,除主键字段外,其他属性 `必须` 设置 `setter` 方法。 + + +## 控制器 + +控制器方法 `应该` 只包含以下三个职责: + +- 验证输入参数有效性 +- 组织数据,调用 Service +- 对调用 Service 返回的数据根据需求调整数据格式返回 + +控制器方法代码行数 `应该` 不超过 80 行,超过 80 行很可能需要将处理逻辑写到 Service 中。 + +控制器的方法 `应该` 只调用 Service ,不能调用 Repository 。 + +> 一旦控制器的方法中调用了 Repository 。 后续参与的开发人员就会延续之前的思路继续在控制器中写代码,破窗效应 一旦形成,后续的代码质量将无法控制。 +