slpi1

slpi1


  • 首页

  • 归档

laravel 主流程

发表于 2019-05-06
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
st=>start: index.php入口
e=>end: 结束
define_start_time=>operation: 定义开始运行时间LARAVEL_START
autoload=>operation: composer自动加载
load_app=>condition: 框架载入
instance_application=>condition: 实例化容器
Application::__construct

app_base_binding=>operation: 容器自绑定
app_base_provider=>operation: 容器基础provider注册
Route/Log/Event
app_base_alias=>operation: 容器核心别名定义

bind_base_kernel=>operation: 单例绑定容器核心服务
HttpKernel
ConsoleKernel
ExceptionsHandler

make_http_kernel=>operation: 实例化http处理器
capture_http=>operation: 捕获http请求
kernel_handle_request=>condition: http处理器处理请求
bootstrap=>condition: 容器启动
bootstrap_app=>operation: 加载环境变量配置
加载配置文件
注册异常handler
注册Facades
注册应用providers
启动应用proveider

common_middleware=>condition: 全局中间件过滤
dispatch_route=>operation: 解析路由
match_route=>operation: 匹配路由
instance_controller=>operation: 实例化控制器
route_middleware=>condition: 路由中间件过滤
call_action=>operation: callAction引导执行控制器方法
run_action=>operation: 执行控制器

common_middleware_item=>operation: 全局中间件详情
up&down检查
post包大小检查
空格过滤
空数据转化为null
代理设置

web_route_middleware_item=>operation: 路由中间件详情
cookie加密解密
cookie设置
start session
Error闪存至session
csrf验证
路由模型绑定监听

send_response=>operation: 发送响应
kernel_terminate=>operation: http处理器运行后台任务

st->define_start_time->autoload->load_app
load_app(yes,right)->instance_application
load_app(no,)->make_http_kernel->capture_http->kernel_handle_request
instance_application(no)->bind_base_kernel(left)->make_http_kernel
instance_application(yes, right)->app_base_binding->app_base_provider->app_base_alias(left)->bind_base_kernel
kernel_handle_request(no)->send_response->kernel_terminate->e
kernel_handle_request(yes, right)->bootstrap
bootstrap(no)->common_middleware
bootstrap(yes, right)->bootstrap_app
common_middleware(no)->dispatch_route->match_route->instance_controller->route_middleware
common_middleware(yes, right)->common_middleware_item
route_middleware(no)->call_action->run_action(left)->send_response
route_middleware(yes, right)->web_route_middleware_item

laravel应用执行流程

发表于 2019-05-06

Index

  • 应用入口
    • 第一阶段:容器准备阶段
      • $app对象实例化
      • Illuminate\Contracts\Http\Kernel::class
      • Illuminate\Contracts\Console\Kernel::class
      • Illuminate\Contracts\Debug\ExceptionHandler::class
    • 第二阶段:容器启动阶段
      • Http处理器捕获请求
      • 启动容器
      • 启动服务提供者
    • 第三阶段:请求处理阶段
      • 路由解析
      • 全局中间件过滤
      • 控制器实例化
      • 路由中间件过滤
      • 运行路由方法并响应
    • 第四阶段:terminate

应用入口

请求经过web服务器,路由到index.php入口文件。在入口文件中引入vendor/autoload.php和bootstrap/app.php文件。其中,前者是文件自动加载规则,后者是应用启动文件。

第一阶段:容器准备阶段

在该文件中,首先实例化应用核心容器Illuminate\Foundation\Application,即$app对象,然后单例绑定容器核心服务:

  • Illuminate\Contracts\Http\Kernel::class
  • Illuminate\Contracts\Console\Kernel::class
  • Illuminate\Contracts\Debug\ExceptionHandler::class

$app对象实例化

  • 实例化应用核心容器时,传入应用根目录作为参数。以此目录分别衍生出base/lang/config/public/storage/database/resources/bootstrap等应用目录
  • 然后将应用对象绑定到自身app别名与Illuminate\Container\Container::class接口,同时绑定PackageManifest::class对象,后续用于对composer.json文件的解析
  • 接下来注册基础服务提供者EventServiceProvider/LogServiceProvider/RoutingServiceProvider
  • 最后进行核心别名的定义,

Illuminate\Contracts\Http\Kernel::class

该接口绑定http请求处理器。用于接收并处理来自web的请求

Illuminate\Contracts\Console\Kernel::class

该接口绑定Console处理器。用于接收来自CLI的命令。

Illuminate\Contracts\Debug\ExceptionHandler::class

该接口绑定异常处理器。用于在应用中出现异常时,做出对应的响应。

第二阶段:容器启动阶段

在做完第一阶段的准备工作后,从容器中解析出Http处理器,Http处理器捕获到请求,开始启动容器。

Http处理器捕获请求

捕获请求也就是$request 对象的初始化。

启动容器

在捕获到请求后,需要先启动容器,容器启动的动作由Http处理器触发,并启动由Http处理器定义的启动文件,启动文件列表如下:

  • \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class: 加载环境变量配置.env文件
  • \Illuminate\Foundation\Bootstrap\LoadConfiguration::class: 加载配置目录配置文件
  • \Illuminate\Foundation\Bootstrap\HandleExceptions::class: 注册异常处理函数
  • \Illuminate\Foundation\Bootstrap\RegisterFacades::class: 注册Facade,由config/app.php中定义的aliases和从composer.json中解析出的Facade组成
  • \Illuminate\Foundation\Bootstrap\RegisterProviders::class: 注册服务提供者,由config/app.php中定义的providers和从composer.json中解析出的providers组成
  • \Illuminate\Foundation\Bootstrap\BootProviders::class: 启动服务提供者。

启动服务提供者

在启动服务提供者后,容器就算是完全准备就绪了:容器准备阶段的别名定义,提供了容器可对外服务的接口,而启动服务提供者的过程中,会实例化接口对应的对象,后续执行对象依赖接口时就可以从容器中解析出所需要的对象。

容器启动的重点,在于启动服务提供者,在该阶段完成了大量的基础重要工作。如:文件驱动服务、数据库连接服务、Session服务、Redis服务、视图服务、认证服务、路由服务等,详见config/app.php中的providers部分。其中,路由服务,会加载路由定义文件,生成路由表,供后续路由解析时做匹配查询。

在容器启动完毕后,进入第三阶段,请求处理阶段。

第三阶段:请求处理阶段

路由解析

路由解析,就是根据当前请求,从路由表中匹配出定义好的路由,匹配会从四个方面进行Uri/Method/Host/Scheme。命中路由后进行下一阶段,否则抛出路由不存在的异常。

全局中间件过滤

命中路由后,请求经过全局中间的过滤,全局中间定义在Http处理器中,应用默认全局中间件列表如下:

  • \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class: 检查应用状态
  • \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class: 检查请求数据大小
  • \App\Http\Middleware\TrimStrings::class:过滤请求数据值中的首尾空字符
  • \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class:将空的值转化成null
  • \App\Http\Middleware\TrustProxies::class:配置代理IP

控制器实例化

请求通过全局中间之后,接着就要实例化控制器。因为下一步就要通过路由中间件,路由中间件可能随路由一同定义在路由文件中,也有可能定义在控制器的$middleware属性中。所以,为了收集到完整的路由中间列表,需要先实例化控制器对象,才能从中解析中间件属性。由于控制器的实例化,与路由中间件的过滤存在这样一个顺序关系,导致在控制器的构造函数中,无法获取到在路由中间件中处理的一些状态。比如登录状态,用户认证过程,依赖\Illuminate\Session\Middleware\StartSession::class|\App\Http\Middleware\EncryptCookies::class等路由中间件的执行结果,控制器构造函数执行时,路由中间件还未执行,因此无法获取用户的登陆状态。

因此,不推荐在控制器的构造函数中做太多的逻辑处理,避免因上述原因导致的错误。如果确实有些场景,需要在构造函数中做一些统一的操作,可以用CallAction方法来代替构造函数,见Illuminate\Routing\ControllerDispatcher::dispatch()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Dispatch a request to a given controller and method.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return mixed
*/
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);

if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}

return $controller->{$method}(...array_values($parameters));
}

路由中间件过滤

一般路由中间件都会包含下列几个:

  • \App\Http\Middleware\EncryptCookies::class: cookie加密与解密,解密是前置部分,加密是后置部分
  • \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class:添加程序中定义的cookie到响应中。
  • \Illuminate\Session\Middleware\StartSession::class:启动session
  • \Illuminate\View\Middleware\ShareErrorsFromSession::class:将验证错误信息系添加到session中,见文档表单验证部分
  • \App\Http\Middleware\VerifyCsrfToken::class:csrf检查
  • \Illuminate\Routing\Middleware\SubstituteBindings::class:路由模型绑定解析

运行路由方法并响应

进过上述处理之后,就终于到达了路由方法,也就是我们定义的路由中的控制器方法或闭包方法。执行完毕生成响应返回给浏览器。

需要注意的是,在执行完路由方法之后,还有可能存在后置中间件的方法待执行,比如cookie的加密,关于后置中间件的说明见文档中间件部分。

第四阶段:terminate

terminate是指在响应发送到浏览器之后会执行的方法。terminate方法中非常适合做日志记录的工作,可以完美解决使用log-viewer插件时的log死循环问题。其次,还需要注意的是,在web环境下与Console环境下,terminate方法的区别。在web环境下,Http处理器先执行中间件中定义的terminate方法,然后执行容器$app的terminate方法,而在Console环境下,直接执行$app的terminate方法。

信息管理部PHP小组知识分享

发表于 2019-04-16

Index

  • 编码规范
    • php编码规范
    • 编码习惯
  • 环境搭建
    • 本地集成环境
    • 虚拟机环境
    • docker环境
  • php框架
    • laravel
      • 版本要求
      • 源码分析系列
      • 扩展包
    • YII
  • 最佳实践

编码规范

php编码规范

目前主流的php编码规范是PSR编码规范。 个人意见是不需要严格去抠规范的细节,但是需要理解规范的意义:

项目的目的在于:通过框架作者或者框架的代表之间讨论,以最低程度的限制,制定一个协作标准,各个框架遵循统一的编码规范,避免各家自行发展的风格阻碍了 PHP 的发展,解决这个程序设计师由来已久的困扰。

对我们的意义在于:规范让我们能编写可维护性更高的代码。

规范概览

通过给IDE编辑器安装相关插件,可以达到自动规范化代码的目的,具体教程方法可以在网上搜索一下。

  • phpfpm
  • sublime安装phpfpm及配置

只不过,插件可以解决代码形式上的规范问题,能做的毕竟有限,何况在代码可维护性上还会遇到编码规范所不能解决的问题,我将这类问题归纳到编码习惯当中。

编码习惯

编码习惯是经验的总结,是一个不断补充的列表,具体内容见下面的链接。如果你有比较好的经验总结,也可提出来供大家参考。

  • 编码习惯 持续更新中…

环境搭建

环境要求

  • php7.2

工欲善其事,必先利其器。为了提升开发效率,达到良好的编码体验,需要配置好一套自己熟悉的开发环境。这里推荐三种开发环境的搭建方式,分别是本地集成环境、虚拟机环境、docker环境,并就本人的使用经验来阐述各自的优缺点。

本地集成环境

目前可以免费使用的本地集成环境有很多,本人只用过 Wampserver,可以根据自己的喜好自己做选择。 请注意尽量在官方网站下载集成环境软件,其他来源的软件可能携带病毒或者后门。

优点

  • 环境搭建难度低
  • 系统资源占用低

缺点

  • windows版本的PHP不支持部分扩展
  • 周边服务使用不便或者干脆没有,imagick、redis、nginx等

    点评:
    本地集成环境基本可以满足日常开发的需要,也是开发者本地必备的环境。但随着项目经验的累积,会遇到本地开发环境难以解决的问题。

虚拟机环境

先在本机上安装Vmware等虚拟机引擎,然后创建一个linux虚拟机,再在linux虚拟机上安装开发环境及周边服务。

优点

  • 熟悉linux系统的使用。
  • 很大程度上模拟正式环境。如果遇到正式环境上的bug无法在本地环境重现,可以再虚拟机环境上试试。
  • 周边服务安装简单。众所周知,很多软件在linux上安装就是一个命令的问题。
  • 可以装多个虚拟环境。

缺点

  • 环境搭建麻烦
  • 系统资源占用升高
  • 需要通过ssh工具来对环境进行管理
  • 不熟悉linux怎么使用?? 流下的技术不足的泪水…

点评:
推荐使用。就我的使用经验来看,虚拟机更多的是对本地集成环境的补充,一般不会在开发的时候,将代码部署到虚拟机来运行,而是通过虚拟机来提供redis/es/nginx代理等服务。

docker环境

通过docker来搭建开发环境也是一种方式。首先需要安装好docker引擎,然后需要进行镜像制作、镜像编排来完成开发环境的搭建。

优点

  • 比较贴合当前技术趋势
  • 拥有虚拟机环境的所有优点
  • 无需ssh工具就能体验linux的功能

缺点

  • 系统资源占用很高,可能比虚拟机环境的资源占用还高。
  • 前期工作量大且复杂
  • 需要学习docker相关知识。但是只需要会docker、docker-compose两个命令的使用就可以搭建

点评:
入坑吧,少年!教程

php框架

我部门目前php的技术栈主要是laravel和YII,除部分遗留的系统外,新系统均使用laravel进行开发。

laravel

版本要求

  • laravel 5.5

源码分析系列

  • laravel应用执行流程
  • laravel ORM源码分析
  • laravel 队列部分源码阅读
  • laravel 路由模块源码分析
  • laravel 框架对__call魔术方法的使用
  • laravel Auth源码分析
  • laravel Pipeline源码分析
  • 邮件发送过滤
  • laravel 中间件
  • laravel 文件系统

扩展包

  • log: laravel 日志记录扩展包
  • im-sdk: 公司内部IM软件接口扩展包
  • emp-sdk: 公司人员中枢接口扩展包

YII

最佳实践

  • PHP坑爹函数系列
  • 错误与异常的处理
  • 如何进行代码结构的规划
  • Excel单元格自动合并的实现方案
  • word操作与pdf转码

TODO

  • PHP坑爹函数系列
  • 可配置化
  • 错误与异常的处理
  • 接口返回定义
  • 对象属性修改的注意事项
  • input的作用域
  • 如何进行代码结构的规划

Excel单元格自动合并的实现方案

发表于 2018-09-25

Index

  • 背景
  • 思路
    • 找出合并的隐含条件
    • 可合并区域的寻找
    • 可合并区域的影响
    • 结束条件
    • 其他问题
      • 三角形问题
      • 起始点的合并顺序
      • 我们如何引入自定义条件
  • 点睛之笔-自定义条件
    • 停止行与停止列
    • 继承
    • 合并优先序
  • 实现
    • 基本对象
      • 单元格对象
      • 区域对象
      • 数据源对象
      • 搜索执行对象
      • 停止规则对象
    • 执行过程
      • 合并的初始化与进行
      • 停止行规则

背景

以前在开发有格数据驾驶舱的时候,由于需要展示比较多的表格,而且表格有合并的情况,每个表格的合并规则还不一致。当时需要同时支持导出 Excel 文件的合并,以及返回到接口的数据,供前端展示时合并,这两种情况。通过分析之后,计划通过两种方案来实现:

  • Excel 模板。模板包含要合并的情况,导出时仅填充数据。
  • 动态合并规则。按要求,自动对数据项进行合并。

通过模板的方式来解决这个问题,需要面以下下困难:

  • 如果合并的情况比较复杂,比如前十行与后十行的合并情况不一致,更进一步,如果这个“十”是变化的,那么模板就无能为力了。
  • 返回给前端接口的表格数据,无法共模板这一方案,需要单独想办法解决

由于上述两个原因,决定采用方案二,动态合并规则来实现。总的概括一下我们要解决的问题:

1
任意给定一组二维数据,根据自定义的一些规则,来对这组数据进行合并,并列出所有合并区域的起点与终点。

思路

如何找出需要合并的区域呢?先假设一个无规则的情况;如果有一个表格,你可以自由的对表格数据进行合并,要如何实现?

找出合并的隐含条件

在无规则情况下,其实默认单元格满足下列两个条件,就可以进行合并:

  • 两个单元格的值相等
  • 两个单元格的位置相邻

可合并区域的寻找

那么,合并区域的寻找,可以通过以下过程来展开:

  • 确定起点:确定数据的起点,假设为 O(0,0),标记点 O 为合并的起点
  • 横向检查:检查 O 右侧的点 N(0,1),如果点 O 与点 N 的值相等,那么表示可以合并,继续检查 N 右侧的点 N1,直到 Nx 的值不等于 O 的值,至此,横向检查完毕
  • 纵向检查:检查 O 下方的点 H(1,0), 如果点 O 与点 H 的值相等,那么从 H 点开始,启动第二轮横向检查。如果不想等,那么合并结束,合并区域为 O~Nx-1 。 Nx-1 表示最后一轮横向检查的终点。

这个过程是一次合并区域检查的过程,每执行一个这样的过程,就会得到一个合并区域,记作 Range[O,Nx-1],如果点 O 与点 Nx-1 相等,说明该合并区域就是一个点,可以舍弃掉。

可合并区域的影响

每寻找到一个合并区域 Range[O,Nx-1] ,我们就消除了一个起点,同时,得到了两个新的起点。由于合并区域是一个矩形,矩形有四个点,其中一个是我们选择的起始点,还剩下三个点,由于对角线上的点比较特殊,我们先抛开不谈,还剩下起始点相邻的两个点,这两个点,就是下一次合并的起始点。
假设合并区域的终点为 Nx-1(m,n),那么,由点 O 分裂出两个新的起始点

1
O(0,0) -> O1(0,n), O2(m,0)

由于合并起始点需要进行一个单位的偏移,对角点会偏移成三个点。但这三个点都有可能被相邻两个点的区域所包含,也有可能不会包含。将对角点考虑进来讨论的话,会大大的增加问题的复杂性,但并不会对结果有积极的意义,弊大于利,所以讨论与编码时,都将这个点忽略

接下来,我们继续以 O1/O2 为起始点,分别进行可合并区域的寻找,就能又找到两个可合并区域,以及,分裂成四个新的起始点。

结束条件

根据上述分析,我们会发现,随着可合并区域不断被找出,起始点不断被分裂成更多的起始点。那么这个循环会一直持续下去吗?并不是,当分裂到数据的边界的时候,一个起始点就只会分裂成一个起始点,这时,起始点的规模就会开始收缩。那么,“数据的边界”,包含哪些情况呢?

  • 数据的范围达到给定范围的极限,就是,数据右边或下边没有更多数据了。
  • 数据的范围,触及到某个已找到的可合并区域的边界。显然,由此分裂出的开始点,已经被“寻找过”,并不会产生新的可合并区域。

当所有的起始点,都触碰到数据的边界的时候,查找,就结束了,已找到的可合并区域,就是给定数据中,所有的可合并区域。

其他问题

我们按照上述思路,已经归纳出了方案的基本雏形,只不过在实际使用中,可能并不会达到我们想要的结果,因为这当中忽略了几个比较重要的问题。

三角形问题

如果数据中有一个三角形区域 O(0,0) -> A(0,10) -> B(10, 0) 其中所有的单元格的值都相等,按照上述思路,最终寻找的可合并区域是 Range[O(0,0), B(10,0)],因为我们在可合并区域的寻找过程中,遵循的是“横向合并优先”,如果我们遵循“纵向合并优先”的话,最终需要的可合并区域是 Range[O(0,0), A(0, 10)],然而实际当中,也许这两种情况,都不是我们希望的结果,比如,若我希望合并区域有最大的面积,那么,最终的可合并区域应该是 Range[O(0,0), M(5,5)]。究竟应该如何取舍,实际上取决于“我们的要求”。

起始点的合并顺序

由于起始点是会逐渐分裂增加的,那么,依据什么来决定,哪个起始点优先进入合并队列呢?考虑一种极端情况,第一个可合并区域将整个数据分成了两个部分:可合并区域的数据都是1,剩下部分的数据,都是2。如果称可合并区域为第二象限,那么以右侧的点开始,得到的合并区域是第一象限与第四象限的组合;如果以下侧的点,开始,得到的合并区域是第三象限与第四象限的组合。所以,起始点的选取顺序,也会导致合并区域结果的差异。

我们如何引入自定义条件

到此为止,我们上述的讨论,都不涉及到自定义条件的问题,而这本身就是需求之一。

点睛之笔-自定义条件

如果讨论至于上述的思路,那么这一方案实际上并无太大用处,因为由于最后三个问题的存在,导致最终合并的结果,很有可能并不是我们想要的。其中,第三个问题,它既是一个问题,又是一个需求,那么,考虑在解决该问题的同时,附带解决其余两个问题。我们将上文的思路,归纳成两个过程:

  • 寻找合并区域
  • 循环起始点

从顺序上看,寻找合并区域 先于 循环起始点 ,从因果关系上看,寻找合并区域 会导致 循环起始点 的主体 起始点 起始点的变化。所以我们先考虑从 寻找合并区域 这一过程中,引入自定义条件。 寻找合并区域 分为两个主要过程,横向检查 与 纵向检查,我们引入的条件,应该是能影响这两个过程的结束位置。

对于上述 三角形问题 ,如果我们要求可合并区域的面积最大,可以转化为这样的一组条件:

  • 横向合并到 5 为止
  • 纵向合并到 5 为止

这里的两个条件,就是我们的自定义条件,我们把这类条件,归纳为 停止行/停止列

停止行与停止列

停止行与停止列,是自定义条件的核心。他规定合并区域的寻找,在遇到哪些行与列的时候,就停止。因此,我们只需要在寻找合并区域的逻辑当中,引入对停止行与停止列的检查即可。需要留意的是,停止行与停止列的位置可能是变化的,我们在编码时可能需要考虑到这一点。

继承

停止行与停止列可能需要被继承,他所代表的含义是:表格前面部分的停止规则,极有可能对后面的数据生效,但是根据后面的数据以及规则,可能无法计算出合适的停止规则,这时,直接将前面的停止规则继承过来即可。

合并优先序

我们在讨论 三角形问题 时,提到过 横向合并优先/纵向合并优先 的概念,这是指在寻找合并区域时遇到的顺序问题。在循环起始点时,也存在这么一个问题:即应该以左侧的点开始新一轮的合并区域寻找,还是应该以下侧的点,开始新一轮的合并区域寻找。这是两个合并优先序的问题。

先来看一看起始点的分裂情况,假设有以下分裂过程:

1
2
3
O(0,0) -> [N(2, 0),H(0,2)]

N(2, 0) -> [N1(5, 0), H1(2,3)]; H(0, 2) -> [N2(1, 2), H2(0, 4)]

起始点出现的顺序是

1
N - H - N1 - H1 - N2 - H2

分布大概如下表所示

0 O(0,0) 1 2 N(2, 0) 3 4 5 N1(5,0)
1 — — — — —
2 H(0,2) N2(1,2) — — — —
3 — H1(2,3) — — —
4 H2(0,4) — — — — —
5 — — — — —

如果以开始点出现的顺序进行合并,通过相对位置可知,点 H1 可能会被包含在 N2 开始的可合并区域中。但是按点的顺序来看,H1 的合并先发生,由于已合并区域会形成边界,这时 N2 进行合并的话,不可能再次包含点 H1。

通过以上示例可以看出,起始点的合并开始顺序,确实会影响最终的合并结果。因此,在每次合并完成后,都需要对已存在的点和新生成的两个点,进行一次排序,用以决定究竟哪个点该进入下一次的合并。

每个点都有一个横坐标与纵坐标,很容易想到的方法是,仅比较每个点的横坐标,越小的点,越先开始合并;或者仅比较每个点的纵坐标,越小的点,越先开始合并。但如果有两个点的某个坐标相同,又恰巧以该坐标决定合并顺序呢,很容易想到,比较应该同时结合横坐标与纵坐标,但以某一项为主;就像如果以横坐标为主,那么横坐标的值充当十位,纵坐标的值充当个位,用以区分其权重。

显然,如果横坐标的排序权重高,我们称为“横向合并优先”,那么每次分裂之后,横向的点都会进入下一个合并队列,纵向的点,进入等待队列,直到横向的数据达到数据范围的极限,此时,等待队列中的开始点,类似下列情况:

1
Nn - H1 - H2 - H3 - - - Hn

横向数据范围越大,Hn 中 n 的值越大,等待合并的点越多。如果纵坐标的排序权重高,我们称为“横向合并优先”,情况依然类似。

其实,只要合并不是无序的,无论是横向优先,还是纵向优先,对合并结果的影响并不是很大,尤其是在停止行规则充分时,基本可以达到相同的合并结果。不过因为横向与纵向数据范围的差异,可能导致待合并队列中点的个数差异,进一步影响点排序的效率,因此,此处可以有一个优化,来使排序的效率增加,就是以数据范围小的坐标作为合并优先序。

实现

基于上述思想。我们简单理一下,该如何编码来实现本功能。

基本对象

单元格对象

该对象用来表示点,他需要有一个横坐标属性,纵坐标属性,还需要能计算出左侧的点,右侧的点,以及两个点是否是同一个点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Cell
{

protected $x;
protected $y;

public function __construct($x = 0, $y = 0)
{
$this->x = $x;
$this->y = $y;
}

public function getX()
{
return $this->x;
}

public function getY()
{
return $this->y;
}

public function nextHorizonCell()
{
return new Cell($this->x + 1, $this->y);
}

public function nextVerticalCell()
{
return new Cell($this->x, $this->y + 1);
}

public function equal(Cell $cell)
{
return $this->x == $cell->getX() && $this->y == $cell->getY();
}
}

这个实现并没有任何对点的值的表示,为何要这样处理,我们放在后面来说。

区域对象

区域表示的是一个范围,他有一个开始的点,与结束的点。同时他还需要有获取分裂后的点的能力,需要有判断点是否落在区域中的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

use Cell;

class Range
{
public $start;
public $end;

public $active;

public $maxX = 0;

public function __construct(Cell $start, Cell $end)
{
$this->start = $start;
$this->end = $this->active = $end;
}

public function haveCell(Cell $cell)
{
$cellX = $cell->getX();
$cellY = $cell->getY();

$startX = $this->start->getX();
$startY = $this->start->getY();

$endX = $this->end->getX();
$endY = $this->end->getY();
if ($cellX >= $startX && $cellX <= $endX && $cellY >= $startY && $cellY <= $endY) {
return true;
}
return false;
}

public function isCell()
{
if ($this->start->getX() == $this->end->getX() && $this->start->getY() == $this->end->getY()) {
return true;
}
return false;
}

public function horizonTouchAllow(Cell $cell)
{
if ($this->maxX != 0 && $cell->getX() > $this->maxX) {
return false;
}
return true;
}

public function markMaxHorizonTouch()
{
$this->maxX = $this->maxX == 0 ? $this->active->getX() : min($this->maxX, $this->active->getX());
}

public function getValue()
{
return [
[$this->start->getX(), $this->start->getY()],
[$this->end->getX(), $this->end->getY()],
];
}

public function nextRowFirst()
{
$x = $this->start->getX();
$y = $this->end->getY() + 1;
return new Cell($x, $y);
}

public function nextColumnFirst()
{
$x = $this->end->getX() + 1;
$y = $this->start->getY();
return new Cell($x, $y);
}

public function nextMergeStartCells()
{
$nextRowFirst = $this->nextRowFirst();
$nextColumnFirst = $this->nextColumnFirst();
return [
$nextColumnFirst,
$nextRowFirst,
];
}

}

数据源对象

数据源对象,就是给定的初始二维数据。只不过我们在编码时,不应该把它具体化,只需要知道,这个对象需要提供哪些功能。因此,数据源对象是什么。我们并不关心,但他需要实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Cell;

interface RepositoryInterface
{
// 获取点的值
public function getValue(Cell $cell);

// 获取数据横向范围
public function getWidth();

// 获取数据纵向范围
public function getHeight();
}

这里来解释一下,为何单元格对象并不能获取自身的值?因为源数据有可能需要一个很大的存储空间,如果单元格能获取到值,必然需要在某处引用这一数据源,此时,Cell 类会对外部产生一个依赖,并且需要在实例化时主动传入该数据对象。而在执行过程中,会出现大量的点对象,由此可能导致内存占用增加,所以,将单元格的取值过程转嫁给数据源对象。

搜索执行对象

对给定数据源执行搜索,其中包含代码主要的逻辑部分。

停止规则对象

停止规则对象依赖给定数据源,用以计算出动态的停止规则。

执行过程

合并的初始化与进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

public function start()
{
// 将点 O(0,0) 放入横向合并队列
$this->horizonMergeQueue[] = new Cell;

// 开始检查合并
$this->catchMergeRange();

// 返回合并区域列表
return $this->mergedRanges;
}

public function catchMergeRange()
{
// 判断下一个要合并的起始点
$startCell = $this->getNextMergeStart();

// 如果不存在,表示合并结束
if ($startCell) {

// 如果点不存在于已合并的区域中
if (!$this->merged($startCell)) {

// 初始化待合并区域
$range = new Range($startCell, $startCell);

// 可合并区域的寻找 - 区域终点搜索
$range = $this->touchRange($range);

// 判断区域是否是一个点,如果不是,则加入已合并区域列表
if (!$range->isCell()) {
$this->mergedRanges[$this->getRangeId($range)] = $range;
}

// 起始点分裂
list($cellHorizon, $cellVertical) = $range->nextMergeStartCells();

// 将点$cellHorizon 加入横向合并队列
if ($this->isExistsCell($cellHorizon)) {
$this->horizonMergeQueue[$this->getCellHorizonIndex($cellHorizon)] = $cellHorizon;
$this->makeExtendStopRule($cellHorizon);
}


// 将点$cellVertical 加入纵向合并队列
if ($this->isExistsCell($cellVertical)) {
$this->verticalMergeQueue[$this->getCellVerticalIndex($cellVertical)] = $cellVertical;
$this->makeExtendStopRule($cellVertical);
}
}

// 启动下一个起始点的合并
$this->catchMergeRange();
}
}


public function touchRange(Range $range)
{
// 区域内 - 横向检查
$range = $this->touchCellHorizon($range);

// 区域内 - 纵向检查
$range = $this->touchCellVertical($range);
return $range;
}

/**
* 纵向检查
*/
public function touchCellHorizon(Range $range)
{
$nextHorizonCell = $range->active->nextHorizonCell();

// 达到纵向数据范围的极限,检查停止
if (!$this->isExistsCell($nextHorizonCell)) {
$range->end = $range->active;
return $range;
}

// 纵向检查通过,启动横向检查,在这里检查停止行规则
if ($range->horizonTouchAllow($nextHorizonCell) &&
!$this->atStopColumn($nextHorizonCell) &&
$this->getValue($range->active) === $this->getValue($nextHorizonCell)) {
$range->active = $nextHorizonCell;
return $this->touchCellHorizon($range);
} else {
// 否则检查停止
$range->end = $range->active;

$range->markMaxHorizonTouch();
}

return $range;
}

/**
* 横向检查
*/
public function touchCellVertical(Range $range)
{
$nextRowFirst = $range->nextRowFirst();
if (!$this->isExistsCell($nextRowFirst)) {
return $range;
}

// 横向检查通过,继续下一轮检查,在这里检查停止行规则
if (!$this->atStopRow($nextRowFirst) && $this->getValue($range->start) === $this->getValue($nextRowFirst)) {
$range->active = $nextRowFirst;
return $this->touchRange($range);
}
return $range;
}

/**
* 获取下一个起始点
*/
public function getNextMergeStart()
{
if (empty($this->horizonMergeQueue) && empty($this->verticalMergeQueue)) {
return false;
}

if (($this->mergeDirect == self::HORIZON_DIRECT && !empty($this->horizonMergeQueue)) || empty($this->verticalMergeQueue)) {
return $this->getNextMergeStartOfQueue($this->horizonMergeQueue);
} else {
return $this->getNextMergeStartOfQueue($this->verticalMergeQueue);
}
}

/**
* 去除起始点时,先进行一次排序
*/
public function getNextMergeStartOfQueue(&$queue)
{
krsort($queue);
return array_pop($queue);
}


// 是否是停止列
public function atStopColumn(Cell $cell)
{
return $this->stopRule->atStopColumn($cell);
}

// 是否是停止行
public function atStopRow(Cell $cell)
{
return $this->stopRule->atStopRow($cell);
}

这里只列出了主要的检查逻辑。

停止行规则

停止规则对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class StopRule
{
use ExcelBaseOperate;

private $exporter;
protected $stopColumn = [];
protected $stopRow = [];

public $extendStopRows = false;

public $extendStopColumns = false;

public function __construct($exporter)
{
$this->exporter = $exporter;

if (isset($exporter->extendStopRows) && $exporter->extendStopRows) {
$this->extendStopRows = true;
}

if (isset($exporter->extendStopColumns) && $exporter->extendStopColumns) {
$this->extendStopColumns = true;
}
}

// 传入一个点对象,用来检查是否处于停止行
public function atStopColumn(Cell $cell)
{
// 如果数据源对象实现了规则,则获取数据源的规则,并解析停止结果
if ($this->exporter instanceof WithStopRule) {
$rule = $this->exporter->stopColumns();
$shouldStop = $this->parserStopColumnRule($rule, $cell);

if ($shouldStop) {
return true;
}
}

if (in_array($cell->getX(), $this->stopColumn)) {
return true;
}
return false;
}

// 传入一个点对象,用来检查是否处于停止列
public function atStopRow(Cell $cell)
{
// 如果数据源对象实现了规则,则获取数据源的规则,并解析停止结果
if ($this->exporter instanceof WithStopRule) {
$rule = $this->exporter->stopRows();
$shouldStop = $this->parserStopRowRule($rule, $cell);

if ($shouldStop) {
return true;
}
}

if (in_array($cell->getY(), $this->stopRow)) {
return true;
}
return false;
}

public function addStopColumn($index)
{
$this->stopColumn[$index] = $index;
}

public function addStopRow($index)
{
$this->stopRow[$index] = $index;
}

// 按数据源规则解析停止结果
public function parserStopColumnRule($rule, Cell $cell)
{
// 规则为布尔值,表示始终停止或不停止
if (is_bool($rule)) {
return $rule;
}

// 规则为数组,表示指定的停止行号或ID
if (is_array($rule)) {
foreach ($rule as $item) {
if (is_numeric($item) && $cell->getX() == $item) {
return true;
}

if ($this->isColumnName($item) && $this->columnNameToIndex($item) == $cell->getX()) {
return true;
}
}
}

// 规则为一个可执行结构,由数据源自身计算是否需要停止
if (is_callable($rule)) {
return call_user_func_array($rule, [$cell->getX(), $cell->getY()]);
}

return false;
}

public function parserStopRowRule($rule, Cell $cell)
{
if (is_bool($rule)) {
return $rule;
}

if (is_array($rule)) {
foreach ($rule as $item) {
if (is_numeric($item) && $cell->getY() == $item) {
return true;
}
}
}

if (is_callable($rule)) {
return call_user_func_array($rule, [$cell->getY(), $cell->getX()]);
}

return false;
}
}

在进行停止行的判定时,有这样的一部分代码:

1
2
3
4
5
6
7
8
if ($this->exporter instanceof WithStopRule) {
$rule = $this->exporter->stopRows();
$shouldStop = $this->parserStopRowRule($rule, $cell);

if ($shouldStop) {
return true;
}
}

其中 exporter 就是一开始给定的原始数据对象,如果该对象实现了 WithStopRule 接口的话,表名该对象定义了自己的停止规则,通过 stopRows/stopColumns 方法获取到原始数据定义的停止规则 $rule,然后将 $rule和当前的点 $cell 传给 parserStopRowRule 方法,计算出在当前点 $cell 是否需要停止。

最终代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/
|----/Concerns/
|--------/ExtendStopColumns.php·············停止列继承
|--------/ExtendStopRows.php················停止行继承
|--------/RepositoryInterface.php···········源数据对象接口
|--------/WithStopRule.php··················停止规则定义接口
|----/Merge/
|--------/Discover.php······················搜索过程定义
|--------/StopRule.php······················停止规则解析
|----/Repositories/
|--------/ArrayRepository.php···············二维数组源数据包装对象
|--------/WorksheetRepository.php···········Excel工作表源数据包装对象
|----/Table/
|--------/Cell.php··························基础点对象
|--------/Range.php·························基础区域对象
|----/Table/
|--------/ExcelBaseOperate.php··············行与列操作方法

完整代码请转gitlab.uuzu.com/songzhp/laravel-excel-merge

服务端支持跨域请求

发表于 2018-03-25

解决 js 跨域的一种方式是直接在服务端设置允许跨域请求,具体原理及规范可以参考HTTP 访问控制( CORS )。问题的关键在于,在创建跨域请求时,请求首部会带上额外信息,这部分不需要手动设置;服务端接收到跨域请求后,设置必要的响应首部信息,完成跨域的请求。

具体设置如下:

  • Apache

    1
    2
    3
    4
    Header set Access-Control-Allow-Origin *
    Header add Access-Control-Allow-Headers "origin, content-type, authorization"
    Header always set Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS"
    Header set Access-Control-Allow-Credentials true
  • Nginx

    1
    2
    3
    4
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Headers' 'Authorization,Origin,Content-Type';
    add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';

如果需要限制能进行跨域请求的域,在 nginx 中可以进行如下设置:

1
2
3
4
5
6
7
8
9
set $set_cross_origin 'http://www.a.com';
if ($http_origin ~* 'https?://(api.a.lar|localhost:4200)') {
set $set_cross_origin "$http_origin";
}

add_header 'Access-Control-Allow-Origin' '$set_cross_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Origin,Content-Type';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';

注意
在服务器是 nginx 时,当响应状态码为40x、50x等错误码时,指令 add_header 会失效。
如果 nginx 的版本大于 1.7.5,可以指定第三个参数 always 来修正这问题;如果版本比较低,需要加入其它模块来完成响应首部信息的添加。具体可以参考Module ngx_http_headers_module、Headers More

2017年终总结

发表于 2017-12-19

工作成绩

2017年主要参与了SDK的维护与升级,H5SDK的开发与维护,聚合SDK的开发与维护,说说英语的相关功能的参与。

经验教训

SDK的维护与升级,从第一版到第二版有很大的改进。第一是用户体系的升级:经历了从用户名登录到用户名或手机登录的的过程。这一功能的改进,主要是为了解决用户名密码注册流程繁琐,随机注册容易忘记账号的问题。通过手机直接注册,一来可以省去输入密码的环节,二来可以解决用户忘记账号的情况。我觉的在实现过程中出现了一些状况,主要是用户名和手机两个字段的冲突问题。应该一开始就区分注册的来源与登录的来源,比如通过验证码的话,必然是通过手机号进行的操作,那么与之对应的唯一标示字段就是mobile,如果是通过密码进行的操作,与之对应的唯一标示就是用户名。执行这一策略也许会在操作流程上引入一些环节,但是可以避免在系统中引入bug的可能。第二是新增游戏盒子的功能,盒子的定制功能的引入,如果要贯彻下去,应该是要有很大的技术资源投入的,否则不足以支撑比较广泛的定制需求,而基础的定制功能对需求方来说会比较鸡肋。所以现在盒子基本上是只有自己在使用的一个功能。第三是关于SDK后台的问题,随着项目的扩大,功能的增多,后台集成的功能越来越多,对于非技术的使用者来说,不一定了解每一个功能如何使用,对于新进来的管理员来说,需要重复的教。从角色的功能的角度出发,可以按照每个角色的权限范围来做一套系统的使用说明文档。

H5SDK基于SDK已有的功能来进行。由于一开始对目标与需求的不明确导致后续的一系列问题。第一是h5的回调通知与原有游戏App的回调通知格式不一致,导致在与游戏厂商对接的过程中需要做两份不同的文档,给对接的过程增加困扰。第二是一开始没有考虑到h5游戏同时会有App包的情况,在分发的环节出现两个入口的问题,一个apk下载入口,一个web在线入口,并且在对接的时候游戏方要对接两套文档。而且现在已经累积了很多上线的H5游戏,从根本上来解决这个问题变得比较困难了。

在开发H5SDK的过成功,使用到了前端框架VUE,掌握了基本的使用。VUE适用于前端驱动的项目,需要分离前后端,与App的通讯类似,但是由于浏览器端的开放性,在浏览器端做比较复杂的加密并不现实,需要引入其他的安全保障机制。关于VUE的一些使用心得,有一下几点

  • 比较轻量级,有比较全面的中文文档支持,入门比较方便。
  • 在项目构架层面没有做出太多规范,如果使用者在这一方面的能力不足,会随着项目规模的扩大变得越来越难维护。
  • 建议在一些小的项目中做尝试,累积经验。

在做聚合SDK的开发前期,对这一项目的目标与流程有一个完整深入的了解,结合前期SDK开发过程中的经验,规避了一下可能会踩到的坑。项目开发过程中,始终贯彻的一个理念是解构。解构整个流程分为两大部分,与渠道的对接和与CP的对接。两个过程由两个相互解耦的模块负责:AgentSdk/PSdk。AgentSdk 负责平台与渠道的对接过程中的签名、验签、参数转换等功能。PSdk负责与CP对接过程中的签名、验签、参数转换等功能。开发过程中先按照思路整理出必要的文档,随着项目的推进不断对文档进行修改与完善。最终开发完毕,重复使用文档到内部对接、外部对接的过程中去,是一次比较愉快的编程体验。

由于前期需求中有web聊天的功能,找到了一些开源的框架来完成,期间接触到Angular。Angular与VUE是统一类框架。关于Angular的一下使用心得:

  • Angular新版本是基于TypeScript来实现,对开发者更友好。
  • Angular框架本身包含了很多模块与组件,功能非常强大。
  • Angular框架本身在项目构架层面做了很多的工作,开发过程中参考文档来实现,可以是项目进展更顺利,对于大型的前端项目来说是比较好的选择。

在工作之余了解了一下laravel,并阅读了部分模块的源码,对laravel有了一个比较基础的认识。laravel借鉴了很多其他语言的优秀框架的实现,包括container,provider这些概念,以及依赖注入的实现等等。laravel的编码非常规范,用到的一些设计模式和一些编程技巧,用很大的使用价值。即使不能使用到项目中,也建议有时间去了解一下,是非常好的学习资料。

今后打算

多学多看多动手,敢于尝试,从各个方面积极寻找可以提升工作效率的更好的手段。

个人建议

  • 线下小组分享的内容要讨论提取精华后运用到工作中去。
  • 可以尝试将前后分离的实践
  • API通讯过程中可以采用其他的安全措施,目前RSA的加密方式,对于带参数调试的情况支持不够。
  • API版本与文档的管理

php返回json数据,int型字段显示为string型的问题

发表于 2016-10-15

开发过程中遇到,同一个接口在不同环境下返回格式不一致的问题。由于前端使用TypeScript开发,对数据类型敏感,本来应该是int型的数据,接口返回格式表示却为string型,导致运行报错。

由于在本地测试没有问题,在服务端返回错误,基本确定为开发环境导致的错误。通过在网上搜索相关类型的问题,得出是 mysql 引擎返回数据时导致的问题。
可能导致该问题的原因有:

  • PDO 进行链接时的参数设置:ATTR_EMULATE_PREPARES = false, ATTR_STRINGIFY_FETCHES = false
  • php 扩展安装不正确:php-mysql 扩展换成 php-mysqlnd

本人遇到的情况属于第二种,第一种情况并未做进一步测试,替换成功后问题解决。

1
yum remove php71w-mysql && yum install -y php71w-mysqlnd

用户相关接口

发表于 2016-08-19
sealtalk用户相关接口文档
阅读全文 »

php 中的引用计数、写时复制、写时改变

发表于 2016-05-19

php 中的这三个概念涉及到php 变量的实现,zval结构:

1
2
3
4
5
6
typedef struct _zval_struct {
zvalue_value value; // 保存变量的值
zend_uint refcount; // 变量引用数
zend_uchar type; // 变量类型
zend_uchar is_ref; // 是否引用
} zval;

其中,refcount、is_ref是问题的主角。

引用计数

  • 例一
    1
    2
    3
    4
    <?php
    $var = 1;
    $var_dup = $var;
    ?>

在上述代码中,第一行创建一个变量$var,并为其赋值1。实际上是创建了一个zval 的数据结构,并将变量$var 与之关联,此时zval 的 refcount=1。第二行将变量$var 赋值给新的变量$var_dump,在这一步中,$var_dump实际上也是与前文中的zval 数据结构进行关联,refcount加一。引用计数在赋值的时候发生。这样处理的结果是可以节省内存开销。

阅读全文 »

php的curl使用

发表于 2015-10-05
php,curl
阅读全文 »
1234
slpi1

slpi1

PHP,Laravel,thinkPHP,javascript,css

37 日志
6 标签
© 2021 slpi1
由 Hexo 强力驱动
主题 - NexT.Mist