云客Drupal8源码分析

前言

Drupal是一个非常优秀的网站系统,可以说她是一个网站应用开发框架,也可以说是一个cms,她在世界范围内被广泛使用,最为人所知的是美国白宫、联合国等知名机构的官方网站使用了她,随着Drupal8的来到,她又达到了一个全新的高度,全面的由面向过程开发转为面向对象开发,代码全部重写,实现几乎和以前的版本完全不同,所以她可以作为一个全新的起点去开始drupal之路,而不管之前是否是drupal的用户;drupal是一个积极融入php大社区的系统,大量采用php大社区已经存在的优秀的组件,使得你在学习drupal8的过程中收获颇丰。

drupal8的中文资料正在不断累计,国内社区在不断壮大,前天(2016年9月11日)我刚刚参加完drupal深圳社区的聚会,感受到社区的活跃,在聚会上做了一个Drupal8入门的分享,很大概的讲了一些内容:查看请点击

我网名云客,“云游天下,做客四方”之意,对未知的事物常怀好奇之心,喜欢到处走一走看一看,就像天上的云一样自由飘荡俯瞰大地,走到drupal这里想起写云客drupal源码分析系列技术分享博客,一方面帮助别人贡献于社区,一方面鞭策自己,争取每周一篇

计划这一系列分享是按主题来讲的,如果你在追踪drupal8的代码执行流程,那么你会发现分享的顺序就是drupal执行的顺序,如果大家喜欢,欢迎留言以示鼓励,可以转载但需注明出处

BY:云客 QQ群203286137  time:20160913

论坛: 
标签: 
Drupal 版本: 

云客Drupal8源码分析 之 自动加载器与Composer

自动加载器:

drupal8启动的第一步就是创建自动加载器,自动加载器是什么玩意?它是怎么产生的?

在面向对象的php程序开发的时候,要实例化一个类对象则需要先加载类定义文件,当 php发现并没有包含类定义文件时,并不会立即报错,它会去一个列队里面依次调用里面定义的函数或者方法,如果在这个过程中类定义文件被加载了,则返回继 续实例化对象,程序可以没有问题的继续执行,否则程序报错,那么这个列队里面的函数或方法是怎么来的呢?它是由用户定义好,然后通过 spl_autoload_register()注册进去的,这就是php的自动加载机制,spl_autoload_register()的使用方法请 见http://www.php.net/manual/zh/function.spl-autoload-register.php

drupal8就使用了这个自动加载机制,在实例化某个对象的时候,php通过类的完 全限定名称(带名字空间前缀的类名)到文件路径的对应关系自动去 include文件,这个工作被封装在一个对象里面完成,这个对象的类定义文件位 于\vendor\composer\ClassLoader.php

在\vendor\composer\文件夹下你会看到如下几个文件:

autoload_classmap.php 里面是类到类定义文件的映射关系图

autoload_files.php 里面是全局需要加载的函数

autoload_namespaces.php 里面是PSR0映射关系

autoload_psr4.php 里面是PSR4映射关系

ClassLoader类对象就是凭借里面定义的基本对应关系去查找函数和类定义文件,在程序运行后期可以动态的添加这种映射关系进去

实例化加载器后(实例化过程中已经向spl_autoload_register注册了),drupal8就不需要手动的 include一大堆文件了,省去了大量工作,说到这里你应该明白了什么是自动加载器和它的原理。

如果你看过drupal8的index.php文件可能会奇怪为什么要中转几次才 到\vendor\composer,其实是因为\vendor\composer里面的文件是自动生成的,此外\vendor目录里面的所有文件都是自 动生成的(vendor里是drupal8用到的所有第三方程序库),这是怎么回事?是谁生成了他们?她就是大名鼎鼎的composer

Composer:

composer被用于php程序的依赖管理,简单点说就是现代php项目或多或少会用到第三方程序库,那么如何保持第三方库的更新?如何下载?多个协作者如何保持版本统一?第三方库又互相依赖或多级依赖怎么处理?这需要一个自动化的解决方案,于是composer产生了

composer用于解 决上述问题,它本身是一个用php写成的应用程序,被封装成了composer.phar,运行在php之上,帮助你下载第三方组件库,保持版本统一,产 生自动加载器的源代码等等,drupal8的\vendor目录就是他自动产生的。下面我们来学习一下它的用法:

先安装 composer,所谓安装其实就是下载它的执行文件composer.phar,如果需要方便一点再把它加入操作系统的环境变量,这里为叙述方便,假定 你使用的是window系统,php已经被添加到环境变量中,composer.phar无需添加到环境变量中,实验目录为C:\root\test \composer:

首先下载composer.phar,官网 有几种安装方式,可以用命令行安装,也可以直接下载,打开https://getcomposer.org/download/查看页面底部,Manual Download手动下载,选择最新的一个版本,下载保存到C:\root\test\composer中。

composer是通过 composer.json文件来解析并自动下载第三方库的,下载完成会生成一个composer.lock文件,用于固定协同开发者的第三方版本,关于 composer.json怎么制作那是使用第三方库的项目开发者的责任,请参考官方文档,这里复制drupal8根目录的composer.json、 composer.lock、core文件夹到C:\root\test\composer中,打开命令行,开始菜单>cmd请确保php被添加到 了系统环境变量,运行下面的命令:

  1. cd C:\root\test\composer  
  2. php composer.phar  install 

此时程序开始下载第三方库,并产生自动加载器,多出一个C:\root\test\composer\vendor目录

进去看一看,对比一下于drupal8根目录下面的\vendor是不是一模一样呢?

关于composer的更多介绍请到官方网站https://getcomposer.org/

 

云客20160913 本文为云客原创,qq群203286137 原文地址http://blog.csdn.net/u011474028

Drupal 版本: 

云客Drupal8源码分析 之 请求对象Request及请求堆栈

drupal8是建立在Symfony组件之上,Symfony认为网站系统无非就是一个将请求转化为响应的系统,并以此设计执行流程,drupal8也是如此,所以整个系统运行之初就是建立请求对象,这个对象将贯穿整个程序,被各个模块访问。

建立请求对象也是为适应面向对象开发方式,这个对象将把以前面向过程式程序设计经常使用的系统输入、环境变量、cookies、session等等 数据封装起来,后续程序仅仅面对该对象即可,非常简洁,如无必要,不应该直接去操作$_POST、$_GET、$_COOKIE等等全局内容。为了方便使 用请求对象也加入了一些额外的功能,比如可以添加自定义属性,这样模块间可以很方便的共享一些数据,它就像一艘船,顺流而下,可以带上你给的数据被沿途的 处理节点看到。

一个根据外部输入产生的请求对象,系统根据它产生响应对象,响应可能包含很多块内容,为了产生每一块内容,在内部可以建立很多子请求对象,相对的由外部输入产生的请求对象称为主请求对象,主请求和子请求被放入请求堆栈中管理。

drupal8不经修改的完全使用了Symfony的请求组件,请求对象的类定义位于:\vendor\symfony\http-foundation\Request.php

下面来看看这个请求对象:

产生主请求:

$request = Request::createFromGlobals();

这将把$_GET, $_POST,  $_COOKIE, $_FILES, $_SERVER数据全部封装到请求对象$request中,Request类允许自定义一个工厂去产生Request对象,工厂产生的对象必须是 Request的实例。工厂是一个回调,先需要设置到Request中,如下:

  1. Request::setFactory($callable);  
  2. $request = Request::createFromGlobals();  

这允许你对基本的请求对象进行扩展,对产生的请求对象添加一些额外数据等等。

在工厂里可以实例化一个继承了Request类的类,极大的增加了灵活性。

在Reques中还提供了静态方法:Reques::create($uri, $method = 'GET', $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null)用以根据自定义的uri产生一个子请求。

Reques提供了操作session的能力,注入一个实现了Symfony\Component\HttpFoundation\Session\SessionInterface接口的session对象即可:

  1. $request->setSession(SessionInterface $session);  

对$_GET, $_POST,  $_COOKIE内容的访问全部通过参数包对象进行访问

\vendor\symfony\http-foundation\ParameterBag.php

上传的文件通过FileBag对象访问,服务器环境数据通过ServerBag对象访问,获得的http头数据通过HeaderBag对象访问

一系列的get方法可以得到常用的一些数据

请求堆栈:

在系统运行中通过请求堆栈对象访问请求对象,请求堆栈对象定义了一个关于请求对象的堆栈数据结构(先进后出),在内部它是通过php数组实现,保存了各个请求对象,最底层是主请求。

请求堆栈对象很简单,请看类定义\vendor\symfony\http-foundation\RequestStack.php

BY:云客  time:20160914  云游天下 做客四方

本文为云客原创,qq群203286137 原文地址http://blog.csdn.net/u011474028

 

Drupal 版本: 

云客Drupal8源码分析 之 响应对象Response及Cookie设置

要理解这一部分推荐先了解RFC2616文档,RFC文档就是互联网技术的魂,该文档定义了http协议,里面详细阐述了各类http头的使用,作 为补充材料可以看一看上野宣所著的《图解HTTP》一书,于均良翻译,(题外话:日本人写的技术书籍大多比较踏实,印象深刻的是远山启写的《数学与生活》 简述极限概念的时候比国内高等教育教科书好太多,希望国内多出好书,)

Drupal8使用了symfony框架的http-foundation组件,里面定义了响应对象,文件路径为\vendor\symfony\http-foundation,默认有五个响应对象:

Response:通用响应对象,用于处理一般响应,也是用的最多的响应对象

以下四个用于特殊目的,他们都继承自Response:

BinaryFileResponse:文件响应

JsonResponse:json响应

RedirectResponse:重定向响应

StreamedResponse:流媒体响应

以上四个响应类对基本的Response进行了扩展或修正,这里主要讲述Response:

Response的作用是储存响应页面,及http响应头的管理(这里包括设置cookie),系统的功能就是将请求对象转换为响应对象,响应对象 的形成意味着已经为浏览器准备好了响应,只需要调用$response->send();发送即可,在系统执行过程中会检测是否产生了该对象,比如 调用控制器后,返回的如果是Response的实例则表示控制器直接产生了输出,否则将返回内容当做渲染数组去开启模板引擎

Symfony\Component\HttpFoundation\Response::create($content = '', $status = 200, $headers = array());可以直接创建一个响应对象,

$content为字符串内容或者具备__toString()方法的对象
$status为状态码
$headers为http响应头部,设置的HTTP头在调用send方法时才会被真正的发送

在Response内部响应页面是直接用一个属性存储的,HTTP响应头使用ResponseHeaderBag对象进行管理,cookie用 Cookie对象管理并保存在ResponseHeaderBag的cookies属性数组里面,真正的发送动作在Response对象里面完成

设置一个cookie:

$response->headers->setCookie(new Cookie($name,$value, $expire, $path, $domain, $secure, $httpOnly));
其中$expire可以是表示时间的字符串、时间戳、DateTime对象

Response具备__toString()方法,因此你可以直接echo $response;看一看输出内容和HTTP头

响应对象对缓存头的控制:

$response->setCache(array $options);参数$options是一个缓存指令数组,键名为缓存指令(默认仅支持这些缓存指令:'etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'),键值为对应的值。

此外Response提供了大量的常用方法,请参看类定义

本文为云客原创,qq群203286137  作者地址http://blog.csdn.net/u011474028

Drupal 版本: 

云客Drupal8源码分析 之 Session系统

Session在网站中扮演非常重要的角色,储存临时用户数据、登录数据等等都用到了它,Drupal8使用到了Symfony的Session组件,该组件非常强大灵活,drupal8在此基础上有所改造和扩展,要理解Symfony的Session组件让我们先从原生php的Session机制说起:

php原生的Session采用服务器文件系统储存用户会话数据,这对一般小型网站足够了,但php做的远非如此,它提供了一整套机制让用户可以自定义Session的实现,比如加密储存、数据放数据库等等,我们看一看Session是如何实现的:

实 现Session有7个方法open, close, read, write, destroy, gc , create_sid,不管是原生、php扩展、还是用户自定义机制都是使用它们,使用session_set_save_handler()函数注册 后,Session机制就会调用这些方法使用它们内部定义的逻辑去储存会话数据,关于详细信息请看官方文档,在php5.4开始php定义了Session处 理器接口SessionHandlerInterface以及此接口的默认实现 SessionHandler ,用户可以自己去实现该接口,接口定义了以上7个方法,这些方法会被php内部自动调用,无需用户干预,php会把调用结果填充到$_SESSION超全 局变量里,这个接口的实现这里把它称为存储处理器,有了它我们就可以在里面实现数据库存储逻辑等等了。

下面来看一看Symfony的Session组件是怎么运作的,先看一张图:

symfonysession.jpg

整个Session系统向用户提供一个统一的界面,由 SessionInterface接口定义,在内部分为两大部分:

SessionBagInterface:数据包接口,用于管理最终用户的数据,这里称之为数据包管理器

SessionStorageInterface:储存处理接口,用于处理数据的储存、Session ID分配等,这里称为储存管理器

数据管理器默认提供了三大类:

AttributeBag:属性包,通常使用的数据就存放在这里

FlashBag:闪存包,应对一些特殊用途,数据只能被取出一次,用后即毁

MetadataBag:元数据包,用于存储Session数据本身的一些元数据,比如Session创建的时间、最后使用时间、生命周期

在SessionBagInterface接口里面我们可以看到两个属性:Name和StorageKey,Name好理解,它用于标识这个数据包,但StorageKey是什么意思?其实数据包里面的数据都是储存在$_SESSION里面,数据包管理器本身只是储存一个引用而已,真正的数据在$_SESSION[$StorageKey]里。每个包都对应着$_SESSION的一个子数组,这样就实现了Session也可以和其他第三方软件兼容,包是如何和$_SESSION[$StorageKey]关联的呢?这就是接口里面initialize(array &$array)方法的工作。

下面看一看储存管理器:

Symfony默认实现了一个储存管理器:NativeSessionStorage,它封装了php的原生Session使用,使用php原生的储存处理器SessionHandlerInterface

我们在示例化Session的时候可以传入自定义的储存管理器及包管理器,构造函数如下:

__construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null)

在不提供任何参数时直接使用php原生机制,不同的是它已经被包装成了面向对象方式

  1. use Symfony\Component\HttpFoundation\Session\Storage\Session\Session;  
  2.   
  3. $Session=new Session();  
  4. $Session->set($name, $value);  

以上就是Symfony的Session组件原理。

drupal8有所改进,包括储存Session到数据库,详见\core\lib\Drupal\Core\Session

它涉及到用户账户接口,另外再讨论

本文为云客原创,qq群203286137  作者地址http://blog.csdn.net/u011474028

Drupal 版本: 

云客Drupal8源码分析 之 服务容器及Symfony依赖注入组件

迟迟未写这个主题是因为它太重要,以至于是drupal8系统运行的一个阶段性标识,它贯穿整个系统,服务容器及Symfony依赖注入组件是drupal8系统的中枢,学习的重中之重

很多新同学可能对“服务容器”、“依赖注入”这样的词感觉陌生,其实非常简单,只是名字玄乎而已,下面解释一下:

何为依赖注入?当一个对象的运行要依靠另外一个对象的帮助,那么就是依赖,把这个依赖的对象保存到本对象的属性中以便随时调用,那么就是注入了,加起来就是“依赖注入”这个词的来历,举个详细的例子:比如一个字符串格式验证器对象Validator,可以验证电子邮件、身份证号码、电话号码等等,但它自身不实现任何验证方法,每一类验证由一个专门的对象负责,比如电子邮件格式验证由mailer对象完成,通过Validator对象的set方法或addValidator方法将mailer或其他验证器对象保存到Validator的属性里面,在Validator对象中就可以调用不同的验证器来完成工作了,这就是将依赖注入到内部属性里面,它就是依赖注入的全部了,是不是很简单?其实这就是一种软件设计模式而已。

在一个大型系统中会有许许多多的常用对象,更进一步的,我们将这些常用对象统统保存到一个超级对象中,要用的时候通过对应的标识符取出使用即可,这样是不是很简洁方便呢!在drupal8中有五百多个常用对象,它们就被保存到了一个超级对象中,这个超级对象就叫做容器,这些常用对象按计算机科学术语就叫做服务(就像计算机操作系统中最常用的程序被一直后台执行,在window中就叫做服务,比如在win7中是右击我的电脑-管理-服务和应用程序-服务,就列出了系统所有的服务),所以这个容器又可以叫做服务容器!

到这里你应该明白了“服务容器”、“依赖注入”,名字玄乎而已,先有依赖注入这个设计模式,然后就产生了服务容器这样的结果,在drupal8中大部分程序都是以服务对象的方式保存在容器中,统计了一下默认下载的初始安装有五百多个服务,下面详细看看服务容器。

一个容器(超级对象)里面保存的对象(又叫服务,面向开发而言叙述方便下文有时也称为对象)有两大类,一类是外部实例化好的对象,然后注入到容器里面,另外一类是容器根据对象的定义数据自己实例化出来的对象。在drupal8中大部分是后者,容器对象根据定义数据在需要时才实例化能提高性能及资源消耗。

下面来看一看drupal如何实现容器的:

容器的形成是在\core\lib\Drupal\Core\DrupalKernel中,它称之为drupal核心类,从名字就可以看出,这个类的主要工作就是创建容器,可见容器之重要。

在实际代码过程中:

先是形成一个引导容器,这个容器包含了数据库对象、缓存对象等基本的服务,以供后续容器形成使用,采用这样的设计是简洁起见

然后是构建Symfony容器,这个是容器形成的主要工作,形成后将容器定义数据导出,并保存到缓存中,下次访问将直接从缓存获取定义数据,节省了大量工作

最后是用第二步形成的容器定义数据创建一个最终使用的drupal容器,这个容器又叫运行时容器Drupal 8 run-time container,它是经过简化的Symfony容器,去掉了一些功能,并改进了一些代码提高运行速度

以上就是容器形成的宏观步骤,第二步是容器的本质性形成,它使用了Symfony依赖注入组件(基本是完全使用,drupal的 ContainerBuilder继承了Symfony的ContainerBuilder类,只这些差别:仅允许服务标识符和参数名为小写;不允许@=方式的扩展;抑制弃用警告;为服务设置_serviceId属性;允许冻结容器时使用set方法注入外部服务;添加__sleep方法;详见Drupal\Core\DependencyInjection\ContainerBuilder),除这些不同外是完全兼容Symfony容器,因此我们在drupal8里面定义服务也是兼容Symfony服务定义的,这个过程也是工作量最大,最复杂的过程,理解了它后其他两步就很容易理解了,下面我们就来看看Symfony容器的形成。

先说说服务容器中对象的定义数据,想想在php中要实例化一个对象并让它可用是怎么一个过程呢?

你可以通过一个类定义直接实例化一个对象,也可以调用工厂方法产生一个对象;然后要可用,可能还需要通过类似set或add这样的方法为对象设置一些必要的属性,也可能是在构造函数里面传递一些参数;容器里面有很多个对象,如果都全部实例化后再保存在容器里面,那么很不利于性能,所以基本是保存对象相关信息,在获取的时候才实例化;有些对象实在太巨大非常消耗资源,对于这样的对象在获取时也不实例化,而是在真正使用时才实例化,获取时得到的只是一个代理而已,这样的技术又叫做延迟加载,或懒加载;有些服务只提供给容器内部使用,不对外,因此服务又有公有私有这样的属性;在每次获取服务的时候可以选择是同一个对象还是重新实例化一个,因此又有共享和不共享的属性;有些服务做类似的工作或具备共同特征,比如上文介绍的验证器,虽然是不同的验证器,但都是对字符串做验证使用,那么需要对这些服务进行组织标记,因此就出现了标签的概念;在容器形成时可能需要对服务对象的参数调整或处理具备某标签的服务,就有了容器编译的概念;服务是外部注入的呢还是容器自己实例化的呢也需要进行标记;服务可能需要参数也可能和其他服务共享参数,那么需要定义参数机制;如果服务需要函数库则需要加载函数库文件;

容器对象的定义数据就是围绕这些内容进行的,先有个印象对后面的学习就轻松许多了。

在Symfony中有个称为Definition的类代表了服务对象定义数据,它位于\vendor\symfony\dependency-injection\Definition.php,提供了操作服务对象元数据的所有方法,上文说了大部分对象都是容器自己实例化的,它就是依据这个对象保存的信息来实例化服务,如果是外部注入的对象,Symfony称为合成物,这样的对象在Definition对象里面没有多余的数据,仅有标识符,和合成物标识。

下面来看一看Symfony容器源代码:

它们都位于\vendor\symfony\dependency-injection下,提供了几个接口来规范容器:

ContainerInterface:基本容器接口

IntrospectableContainerInterface:内省容器接口,继承自基本接口,所谓内省就是看服务是否已经由定义元数据实例化成了对象,该接口在Symfony3.0并入基本接口

ResettableContainerInterface:可重置容器接口,继承自基本接口,提供重置容器功能

TaggedContainerInterface:带标签容器接口,继承自基本接口,为容器增加标签功能

Symfony定义了一个默认容器实现类:

Container:是一个简单的容器,提供了接口要求的基本功能。

我们在创建容器的时候往往需要更多的功能,所以Symfony提供了一个ContainerBuilder,它继承自Container容器,但提供了丰富的功能来帮助构建容器,可以看做是一个融合了容器工厂的容器,所以它的名字是ContainerBuilder容器构建,这个容器也是主要使用的容器。

在上文提到的宏观步骤中构建容器,就是用ContainerBuilder构建一个空容器,然后将服务定义数据添加进去,再设置各种参数、合成物最终形成一个Symfony容器。

Symfony容器构建成功后,将导出(dumper)容器定义数据,这个导出工作是由\core\lib\Drupal\Component\DependencyInjection\Dumper完成的,导出保存到缓存,并用这个定义数据建立drupal容器,也叫作运行时容器,系统后续就是用该容器了,说到这里你可能有一个疑问,为什么不直接使用Symfony容器呢?还要导出再建立那么麻烦,答案是:性能!首先导出的定义数据被整理,并使用php数组形式,再者drupal容器根据自己需要去掉了许多不必要的功能,这样能大大提高速度和节省资源,这里说一个题外话:许多中间供应商都有一个冲动,就是把产品做的尽量强大,满足尽可能多的需要,而在最终产品的使用上许多功能却是用不到的,因此最终产品设计者经常需要对半成品进行瘦身定制,不只是软件设计领域,在生活中其他领域也经常看到,想想是不是这样?

下面说说Symfony容器和drupal容器的区别:

Symfony容器使用Definition对象做服务的定义元数据,而drupal容器使用php数组,想var_dump这个数组满足下好奇心?看最后的容器补充

Symfony容器中的以下功能在drupal容器中都没有了:

addCompilerPass、getCompilerPassConfig、getCompiler、compile:添加编译器,所以drupal容器无法再编译,它是一个已经编译的容器,关于编译下面再讲

merge:合并容器

addAliases、setAliases、setAlias、removeAlias、hasAlias、getAliases、getAlias:操作服务别名

register:注册服务对象

addDefinitions、removeDefinition、getDefinitions、setDefinition、hasDefinition、getDefinition、findDefinition:定义数据操作

findTaggedServiceIds、findTags、findUnusedTags:标签操作

以上这些功能均不能在drupal容器(运行时容器)中使用了

drupal容器不支持的Symfony容器语法:

容器定义数据dumper的时候不允许decorated定义,drupal容器不允许这个,该属性必须在Symfony容器构建阶段定义编译pass解决;

yaml文件不允许@=扩展,这样的语法在drupal中禁用

下面说说什么是容器编译:

我们知道容器中有些服务对象是功能相似的,他们被用标签tags分成一组,举个具体的列子:在对缓存进行访问的时候,需要策略控制,一条策略就是一个服务,drupal模块可以定义自己的策略,只要对这个策略服务打上策略标签即可,这样在访问缓存的时候就会调用这个策略,那么问题来了,是在执行访问检查的时候才去查找这些策略吗?其实是在容器形成的时候就已经查找出了这些策略,并把他们当做参数传给了访问控制策略表对象,这个工作就是容器编译,准确的说容器编译就是根据标签信息建立服务对象之间的关系,或者调整一些容器对象的参数,总之就是对容器里面的服务对象做些补充工作。

在构建容器时,drupal如何给出服务对象的定义数据:

在构建容器的时候drupal通过服务静态定义文件services.yml文件和服务提供器ServiceProvider来为容器添加服务定义文件,相比services.yml而言服务提供器有更多的自由度,它实现ServiceProviderInterface接口,可以完成services.yml的所有工作外,还可以操作容器编译。而services.yml和ServiceProvider是怎么来的呢?是通过Drupal\Core\Extension\ExtensionDiscovery类扫描所有的核心及扩展模块找出来的,这里澄清一个让很多初学者模糊的概念,关于模块的名字,很多人混淆模块所在文件夹名、.info.yml文件名、.info.yml文件里面的name值,看完ExtensionDiscovery类的实现后就会明白文件夹名和模块名无关,文件名才是模块名,里面的name值仅仅用于后台显示。

关于服务容器的补充:

1:在站点设置文件中可以设置运行时容器类,默认是\Drupal\Core\DependencyInjection\Container,格式:$settings['container_base_class']=“\Drupal\模块名\Container”;当你需要特殊功能时可以继承默认的类,定义一些新功能,通过这个设置运行时容器就可以自定义了。

2:要想看一看运行时容器的定义数据是什么样子?drupal8都提供了哪些常用服务?请用phpMyAdmin打开数据库,找到cache_container数据表,下载里面的data字段内容,得到一个扩展名为bin的文件,它就是容器定义数据的缓存文件,内容是经过序列化的,unserialize并print_r即可看到:print_r(unserialize(file_get_contents("cache_container-data.bin")));

3:在容器编译的时候有个特殊的标签:{ name: service_collector, tag: tags_name, call: add},意思是查找所有标签为tags_name的服务,并把它们依次作为回调add的参数,在drupal中有许多这样的用法,非常灵活方便

4:容器这么好,服务对象都在容器里面,在代码里面怎么取得容器?\Drupal::getContainer();即可,在drupal核心DrupalKernel建立完运行时容器后,就将他注入到了全局类drupal的静态属性里面:\Drupal::setContainer($this->container);,因此你可以在任意地方取到容器,并使用里面的服务,比如要操作数据库,取到数据库对象,代码如下:\Drupal::getContainer()->get('database');全局类Drupal也提供了许多常用方法,比如获取数据库又可以这样写:\Drupal::database();,这样的写法在内部就是通过前面的方法实现的,只不过做了包装,更加方便使用

 

本文为云客原创,qq群203286137  作者地址http://blog.csdn.net/u011474028

 

 

Drupal 版本: 

云客Drupal8源码分析 之 HttpKernel堆栈

HttpKernel为何物?从名字可以看出它就是处理http请求的核心,只需要把请求对象传给它,就返回响应对象,一次http访问大体上就算完成了(为什么说是大体上,在实际应用中发送完响应还会做一些类似于析构函数工作的事情),为规范统一HttpKernel的使用symfony为此定义了HttpKernel接口,位于\vendor\symfony\http-kernel\HttpKernelInterface,在drupal8中所有HttpKernel定义类都必须要实现该接口,在Druapl8中HttpKernel对象并不只有一个,而是有多个,储存于堆栈里,从上到下依次执行,优先级高的位于堆栈上层,从顶层开始执行,上层调用下层,形成一个类似链式结构的处理过程,直到有响应对象产生即终止,下层的HttpKernel可能不被执行,下面详细讲一讲这个过程:

先说一说这个HttpKernel堆栈是怎么产生的:

在drupal8中把堆栈里面的HttpKernel对象称之为http_middleware(http中间件),堆栈于服务容器编译阶段确定(容器在Drupal\Core\DrupalKernel中形成),对此堆栈的编译过程定义在\core\lib\Drupal\Core\DependencyInjection\Compiler\StackedKernelPass.php,在编译容器的时候这个CompilerPass收集所有被标记为http_middleware的服务(这样的服务必须实现HttpKernelInterface接口),并依据优先级进行排序,将最高优先级的HttpKernel和这个排好序的数组分别作为堆栈管理对象StackedHttpKernel的第一和第二个参数,在容器中$container->get('http_kernel')取出http_kernel服务的时候取出的就是这个堆栈管理对象了,这里顺便提一下如何定义堆栈中的HttpKernel服务,该服务必须实现HttpKernel接口,并标记为http_middleware,额外可以设置priority(优先级,没有设置默认为0)、responder(响应器,能产生响应对象时设置为真),比如页面缓存HttpKernel服务的定义如下:

 

services:
  http_middleware.page_cache:
    class: Drupal\page_cache\StackMiddleware\PageCache
    arguments: ['@cache.render', '@page_cache_request_policy', '@page_cache_response_policy']
    tags:
      - { name: http_middleware, priority: 200, responder: true }

堆栈里面的HttpKernel对象不一定会产生响应,它可能只是帮助设置一下请求对象,也可能进行一些功能,请求对象会依次从高层传递到低层。此外用户也可以在自己的模块中定义HttpKernel对象,高优先级的HttpKernel可以中断较低优先级HttpKernel的执行。

 

下面我们看一看默认的drupal8安装后堆栈里都有哪些处理核心,为便于理解我画了一张示意图:

初始安装的Drupal8默认有7个处理核心,依据优先级,它们在堆栈中形成7个层,越上层优先级越高,由上往下依次执行HttpKernel的handle()方法,这些HttpKernel可选择的实现TerminableInterface接口,如果实现,它们的terminate()方法也是从上到下依次执行,整个堆栈的实现可以看一看StackedHttpKernel类的实现,位于\vendor\stack\builder\src\Stack\StackedHttpKernel.php,下面分别看一看这7个HttpKernel都干了什么:

1:http_middleware.negotiation( Drupal\Core\StackMiddleware\NegotiationMiddleware)

用于进行内容协商,它会在请求对象里面设置请求的格式,然后调用下一层的handle()方法是它的责任

2:http_middleware.reverse_proxy(Drupal\Core\StackMiddleware\ReverseProxyMiddleware)

为请求对象注入反向代理数据,该数据可以定义在站点配置文件settings.php中

3:ban.middleware(Drupal\ban\BanMiddleware)

它就是管理后台/扩展模块里面的Ban模块,它查询数据库获得不允许访问站点的IP,如果是禁止的ip则终断下层HttpKernel的处理,给出内容为拒绝访问的响应对象,如果你想快速学习模块开发,那么核心的Ban模块是很好的教程,里面有服务定义、权限定义、菜单定义、表单定义、数据库操作、路由定义等等,它可以成为你借鉴的第一个模块开发案例

4:http_middleware.page_cache(Drupal\page_cache\StackMiddleware\PageCache)

缓存层,根据请求对象查找是否有缓存的数据,有则直接产生响应,否则继续下层的处理,并把可缓存的响应缓存起来供以后使用,它其实就是管理后台/扩展模块里面的Internal Page Cache模块,默认是不能关闭的

5:http_middleware.kernel_pre_handle(Drupal\Core\StackMiddleware\KernelPreHandle)

对请求进行预处理,你也许已经研读过DrupalKernel类的代码了,当时可能你疑惑里面的preHandle方法是什么时候调用的,没错,就是这个HttpKernel层的工作,它加载\core\includes里面的许多函数库、加载模块、建立请求堆栈等等,这个层仅仅为下层的处理进行许多准备工作

6:http_middleware.session(Drupal\Core\StackMiddleware\Session)

session系统就是这个时候启动的,并将session对象注入了主请求对象中,便于后续使用

7:http_kernel.basic( Symfony\Component\HttpKernel\HttpKernel)

启动 Symfony的HttpKernel,开始进行最里层的请求转为响应过程,精彩还在继续

 

好了,这就是HttpKernel堆栈的结构和流程原理,它是自服务容器以来的又一大步

by:云客【云游天下,做客四方】

本文为云客原创,qq群203286137  作者地址http://blog.csdn.net/u011474028

Drupal 版本: 

云客Drupal8源码分析 之 缓存系统Cache

在介绍drupal8的缓存系统前我们先了解一下缓存系统的本质及特性,缓存的存在依赖于两个目的:节省资源和提高速度,起不到这两作用则缓存没有存在的必要,当一个结果需要进行大量计算才能得到,而它又不会频繁更新那么缓存结果可以节省大量算力,缓存的是一个结果,这个结果可以存放在多台服务器上面实现负载均衡,从而进一步提高访问速度,在高访问网站中缓存非常重要,drupal8的缓存设计也是围绕这两个目的而设计。

Symfony没有提供缓存组件,drupal8完全自己实现了缓存系统,druapl8的缓存系统不只是缓存响应页面,它还缓存许多系统中间数据,比如容器定义数据、配置数据、扩展数据等等,你觉得有必要缓存的数据它都可以缓存,默认的它使用数据库来存放缓存数据(流媒体、二进制文件等默认不进行数据库储存),在数据库中以cache_为前缀的数据表均存放的是缓存数据,此外有一个叫做cachetags的数据表用来储存缓存系统的维护数据,当手动清理缓存的时候清空他们即可,包括cachetags表,如果只清空某一个缓存表则不要清理cachetags表,清理cachetags会导致所有缓存表里面的数据失效(仅失效而已,不会导致系统错误),当系统运行时间过长时会发现缓存系统累积太多数据,从而降低缓存效率,可以清空他们一下。

drupal8的缓存系统可以进行多种方式缓存数据,不仅仅是数据库,还可以结合配置外部高速缓存等等,下面介绍它的实现原理:

在理解drupal8缓存系统时请记住它实现了两大块内容,一是如何存储与取回数据、二是如何判断缓存数据是否过期及可缓存性质。

关于如何存储与取回数据drupal8有两个概念:cache_bin和cache.backend

bin可以理解为存放数据的箱子、容器、数据池等等,比如在默认实现中数据库的一个缓存表就是一个bin,这个表就是一个箱子,里面是一条一条的数据。backend就是bin的操作者,它是一个对象,负责储存、删除、取回、失效bin里面的数据,为什么取了backend这么个名呢,直译是后端的意思,它相对于前端缓存而已(缓存代理服务器、浏览器缓存等),在系统里面每个bin对应着一个backend,这种对应关系可以在站点配置文件settings.php里面设置(下面讲),无配置的情况下采用默认配置,只需要将bin传给缓存工厂就能自动得到backend

关于如何判断缓存数据是否过期及可缓存性质有三个概念:CacheMaxAgeCacheTags、CacheContexts

CacheMaxAge:代表缓存最大有效时长

CacheTags:缓存标签代表缓存的数据是什么

CacheContexts:缓存上下文代表同一数据在不同情况下的状态,上下文大多来自请求对象(但不是全部),比如请求的语言、数据格式、用户代理、用户、ip、cookie数据等等,同一数据在这些上下文下可以表现出不同的状态。

系统定义了一个可缓存依赖接口如果一个数据是可缓存的,那么可以获得实现该接口的可缓存元数据,里面定义了以上三个概念的值。

下面看一看具体代码实现:

drupal8核心Cache系统在\core\lib\Drupal\Core\Cache,它提供最核心的功能,供其他缓存模块和需要使用缓存的地方使用。

所有的backend都需要实现CacheBackendInterface接口,此接口里面定义了backend可用的公用方法(在drupal8里面所有的命名都有约定规则,接口以Interface结尾,特征以Trait结尾,工厂方法以Factory结尾,一个文件一个类,文件名与类名一致,所以你可以根据文件名大致推断文件的用途,更多约定规则请看社区文档),基于接口的软件设计是大型软件设计的精华之一,用户不需要知道具体实现,针对接口提供的方法使用即可。

drupal8默认提供了以下Backend:

Apcu4Backend、ApcuBackend、DatabaseBackend、MemoryBackend、MemoryCounterBackend、NullBackend、PhpBackend

此外有两个特殊的Backend:

BackendChain将多个Backend结合起来使用,形成一个Backend链

ChainedFastBackend为了改善性能将一个快速Backend和一个一般Backend结合起来使用

以上Backend都由Backend工厂负责实例化(BackendChain除外),默认定义的Backend工厂有:

ApcuBackendFactory、ChainedFastBackendFactory、DatabaseBackendFactory、MemoryBackendFactory、NullBackendFactory、PhpBackendFactory

这些工厂都实现了CacheFactoryInterface接口,该接口很简单,仅仅定义了一个get方法接受bin参数,它返回Backend对象,默认的bin和Backend有一套对应关系,如何自定义这种关系呢?在站点配置文件settings.php里面定义即可,格式如下:

$settings['cache']['bins']['你定义的bin']="缓存工厂服务的服务ID";

默认的对应关系定义在CacheFactory类的get方法里面,源代码如下:

 

public function get($bin) {
    $cache_settings = $this->settings->get('cache');
    if (isset($cache_settings['bins'][$bin])) {
      $service_name = $cache_settings['bins'][$bin];
    }
    elseif (isset($cache_settings['default'])) {
      $service_name = $cache_settings['default'];
    }
    elseif (isset($this->defaultBinBackends[$bin])) {
      $service_name = $this->defaultBinBackends[$bin];
    }
    else {
      $service_name = 'cache.backend.database';
    }
    return $this->container->get($service_name)->get($bin);
  }

这里看一看使用最多的DatabaseBackend,它是默认Backend,负责数据库缓存操作,在DatabaseBackend类里面定义了数据库缓存bin的数据结构,先看一看默认的数据库缓存表,它们都以cache_为前缀,表字段如下:

 

cid:缓存id,一个255以内的ascii字符串

data:缓存的数据内容

expire:缓存到期时间戳,永久为-1

created:缓存创建的时间

serialized:缓存的数据是否经过序列化

tags:缓存标签

checksum:缓存数据有效性校验值

在前面我们说到了关于如何判断缓存数据是否过期有三个概念,那么系统在这个数据表结构里面如何反映出来呢?对应关系如下:

cid对应着上下文依赖,不同的上下文组合得到的缓存cid不一样,使用中的cid和缓存bin里面存储的cid不一样,存储的cid是经过哈希等方法转换得到的一个255以内的ascii字符串,转换过程如下:

 

 protected function normalizeCid($cid) {
    // Nothing to do if the ID is a US ASCII string of 255 characters or less.
    $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
    if (strlen($cid) <= 255 && $cid_is_ascii) {
      return $cid;
    }
    // Return a string that uses as much as possible of the original cache ID
    // with the hash appended.
    $hash = Crypt::hashBase64($cid);
    if (!$cid_is_ascii) {
      return $hash;
    }
    return substr($cid, 0, 255 - strlen($hash)) . $hash;
  }

使用中的cid是由CacheContexts缓存上下文组合得到

 

expire对应着时间依赖,指示过期的时间,这个简单不必多讲

tags对应着数据种类依赖,一条缓存可能由多个标签对应的数据组成,其中任何一个tag对应的数据发生变化都会造成该条缓存失效,每一个tag对应的数据又可能被多条缓存使用,那么当tag对应的数据发生变化时我们如何及时得知这些缓存条目过期了呢?这是drupal8缓存设计的一个精髓,答案就在于checksum有效性校验值,上文说过系统中存在一个缓存维护数据表,表名称是cachetags,里面有两个字段:标签及失效次数,每当一个标签对应的数据产生修改动作,那么失效字段就会加一,而checksum的值就是该条缓存所有标签对应的失效字段值之和,也就是说任意一个tag对应的数据只要产生修改动作,那么就会引起使用到它的缓存校验值变化,系统就可以根据这个来判断缓存是否失效,这个计算校验值和判断的工作是由DatabaseCacheTagsChecksum类来完成的,它计算出当前校验值再和缓存bin里面的校验值比较,不同则缓存失效,往往比失效的校验值大,如果缓存的数据无标签则校验值永远为0。

数据库bin里面每个条目就是一个缓存,drupal8还很贴心的为我们做了进一步的工作,在一个缓存条目里面储存很大的一个数据集,数据集以数组的方式储存在缓存bin的data字段,对这个数据集的操作drupal8提供了CacheCollector类,它实现CacheCollectorInterface, DestructableInterface接口,Destructable接口用于服务在毁灭时做一些收尾工作,类似于析构函数。

以上就是数据库缓存的数据结构及存取情况,下面来看一看数据的可缓存性:

需要缓存的数据对象被注入了可缓存元数据对象,那么它就具备了可缓存性,可缓存元数据对象实现了可缓存依赖接口:CacheableDependencyInterface,该接口里面仅仅定义了三个get方法,分别对应于上下文、标签、过期时间,有了这些方法就可以知道需要缓存的数据对象如何缓存,CacheMaxAgeCacheTags、CacheContexts也称之为可缓存属性,CacheTags、CacheContexts都是字符串表示。在任何网站系统中可缓存数据均有这三方面的依赖。

在\core\lib\Drupal\Core\Cache\Context中定义了常用的上下文依赖。

 

以上就是drupal8缓存系统的核心,下面介绍两个应用核心缓存功能的模块,他们是Page Cache和Dynamic Page Cache,它们都是系统默认提供的模块,且不可关闭,位于core\modules,在系统管理后台-扩展:模块列表中能找到。

Page Cache模块为匿名用户提供页面缓存,在我前面发的《云游Drupal8系列之HttpKernel堆栈》里面有提到,它实现了HttpKernelInterface接口,属于http中间件,缓存的数据位于数据库表<span class="syntax"><span class="inner_sql"><span class="syntax_quote syntax_quote_backtick">cache_render</span></span></span>里面,使用URI和请求格式作为cid,这个模块的bin就是<span class="syntax"><span class="inner_sql"><span class="syntax_quote syntax_quote_backtick">render</span></span></span>Backend是DatabaseBackend。

Dynamic Page Cache模块为任意用户提供动态的缓存页面,数据储存于数据库cache_dynamic_page_cache表,它通过使用事件订阅机制在在动态响应产生时缓存数据,关于事件订阅敬请期待后续云游系列主题。

如果你看到这里,你应该已经大致了解了drupal8的缓存运作机制,明天就是国庆假期,祝大家节日快乐,总算赶在节前出了这篇主题,有不明地方欢迎留言

本文为云客原创,qq群203286137  作者地址http://blog.csdn.net/u011474028

Drupal 版本: 

云客Drupal8源码分析 之 页面缓存PageCache的请求策略RequestPolicy及响应策略ResponsePolicy

在drupal8中提供了两个页面缓存模块,一个是PageCache用于匿名访问时,一个是dynamic_page_cache用于处理登陆用户的页面缓存。他们都有对应的请求策略及响应策略。

那么这两种策略如何运作?作用是什么呢?请看下面

先讲讲用于匿名用户的页面缓存PageCache的请求响应策略:

RequestPolicy:请求策略,用于判定评估请求是否允许利用匿名页面缓存,如果允许则从缓存系统中取数据,反之不能从缓存里取数据而是让系统运算生成数据

ResponsePolicy:响应策略,用于评估系统产生的新鲜的可缓存响应是否需要存放到缓存系统里面,在响应本身是可缓存响应的情况下,这给了用户最后一次机会控制是否缓存

以上就是请求、响应策略的目的,下面来看一看它是怎么实现的:

这一块的内容源代码放在\core\lib\Drupal\Core\PageCache里面,可以看到为请求、响应策略分别定义了接口:

RequestPolicyInterface

ResponsePolicyInterface

内容很简单,只有一个 check方法,一个实例就是一条策略,系统往往需要多条策略,因此系统为请求及响应都定义了链式策略,并定义了链式策略接口:

ChainRequestPolicyInterface:链式请求策略,默认实现为ChainRequestPolicy,可以通过addPolicy方法添加多条策略,这些策略共同作用一个结果,规则为:任意一条策略结果为拒绝则拒绝,在没有拒绝的情况下至少一个允许则允许,否则返回NULL

ChainResponsePolicyInterface:链式响应策略,默认实现为ChainResponsePolicy,可以通过addPolicy方法添加多条策略,这些策略共同作用一个结果,规则为:任意一条策略结果为拒绝则拒绝,否则返回NULL

系统默认提供了一些请求、响应策略:

请求策略默认提供了三个:

CommandLineOrUnsafeMethod:当运行于命令行或不安全的http方法时拒绝

NoSessionOpen:当开启SESSION时拒绝

Drupal\toolbar\PageCache\AllowToolbarPath:它由toolbar模块提供,如果访问的是工具栏页面则允许

响应策略默认提供了5个:

Drupal\node\PageCache\DenyNodePreview:节点预览不缓存

Drupal\image\PageCache\DenyPrivateImageStyleDownload:实体图片预览不缓存

Drupal\Core\PageCache\ResponsePolicy\NoServerError:服务器错误5开头的状态码响应不缓存

Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes:有no_cache选项设置的请求不缓存

Drupal\Core\PageCache\ResponsePolicy\KillSwitch:当KillSwitch被触发时不缓存

那么用户如何自定义策略呢:

定义一个服务,并给标签为page_cache_request_policy或page_cache_response_policy即可

在容器编译时,系统会自动查找被标记为以上标签的服务并加入到链式策略中

 

以上就是PageCache模块的策略介绍,关于dynamic_page_cache动态页面缓存后面补充

本文为云客原创,qq群203286137  作者地址http://blog.csdn.net/u011474028

Drupal 版本: 

云客Drupal8源码分析之php流Streams、公共文件、私有文件

在开始这个主题前我们做一个实验,在你的drupal8模块控制器中加一行代码:

file_put_contents("public://yunke.txt","Streams test");

然后访问这个控制器,看看发生了什么?没错页面上不会有什么改变,但也没有报告什么错误,那这行代码到底干了什么?

作为开发者你应该很熟悉file_put_contents()这个函数,代码意思是将"Streams test"这个字符串写入一个文件中,

可是文件名却是:"public://yunke.txt",它是什么意思?文件保存了吗?保存到哪里去了?

这就是本主题要讲的内容!

不知道"public://yunke.txt",很正常,但你可能知道php://input,没错,就是那个php读取原始post输入的东西

在微信公众账号管理系统中就是用这个来读取收到的原始XML数据,其实public://yunke.txt和php://input差不多,它们源自同一个概念

就是php流及流包装器,由于在一般的开发中很少用到它,所以很多开发者并不熟悉,但它功能强大,是php这门语言的一个重点内容

在java中也有类似概念,如果你对C语言熟悉,那么会倍感熟悉,它或多或少基于ANSI C 标准IO函数簇。

由于php流这个知识点属于php学习的范畴,并不限于drupal源码分析,所以我单独写了篇博客来介绍它:

请看:http://blog.csdn.net/u011474028/article/details/52814049

那是本文的一部分,如果你对流不熟悉请务必暂停去阅读学习后再继续,否则你很难理解后面的内容,在文末附加一个流包装器的例子。

这里开始假定你已经学习完了流相关的知识,来看一看它在drupal8中的运用:

还是从file_put_contents("public://yunke.txt","Streams test");说起

它是在公共文件目录里面建立一个叫做yunke.txt的文件,里面保存了字符串"Streams test",那么这个公共文件目录在哪里?

它可以在站点配置文件中指定,如果你没有指定,那么默认是\sites\default\files,如果你按上面的方法做了,看一看是不是有这个文件?

drupal使用流这个概念来储存公有及私有文件并提供操作上的方便,私有文件可以保存到一个web访客无法通过url访问到的地方,这样就比较安全了

如何在settings.php文件中配置公有及私有文件的主目录:

$settings['file_public_path'] = "./yunke"; //所有公共文件都将保存到站点根目录的yunke文件夹下,若无默认是\sites\default\files
$settings['file_private_path'] = “/home/yunke”; //设置后需要做权限设定,以防止web访客通过url访问到文件,配置中若有此项就会在容器中注册私有流包装器

关于流方面的源码储存在:\core\lib\Drupal\Core\StreamWrapper里面

定义了一个php级别的流包装器接口:PhpStreamWrapperInterface

drupal用的包装器接口(继承自上面的接口):StreamWrapperInterface

LocalStream:抽象的本地流包装器

额外定义了公共流包装器、私有流包装器、临时流包装器、只读流

这些包装器用一个包装器管理器来管理:

StreamWrapperManager:实现了StreamWrapperManagerInterface接口,主要作用是注册流包装器

调用流包装器管理器的代码位于drupal核心:Drupal\Core\DrupalKernel的preHandle方法中,有以下代码:

// Register stream wrappers.
    $this->container->get('stream_wrapper_manager')->register();

而preHandle方法的调用是在多层HttpKernel核心处理中的预处理层:
http_middleware.kernel_pre_handle(Drupal\Core\StackMiddleware\KernelPreHandle)
请看前面的《云客Drupal8源码分析之HttpKernel堆栈》一文

 

以上就是本主题的全部了,下面提供一个流包装器案例,是我从drupal8中抽取出来,做了修改,可以独立使用的一个类,可以研究下它的实现:

 

class yunkeWrapper
{
    public $context;
    public $handle = null;
    public $uri = null;
    protected static $dir = "";
    //public $dir = "dir";//可以是相对路径或绝对路径,空表示当前路径

    public function dir_closedir()
    {
        closedir($this->handle);
        // We do not really have a way to signal a failure as closedir() does not
        // have a return value.
        return true;
    }

    /**
     * @return bool
     */
    public function dir_opendir($uri, $options)
    {
        $this->uri = $uri;
        $this->handle = opendir($this->getLocalPath());
        return (bool)$this->handle;
    }

    /**
     * @return string
     */
    public function dir_readdir()
    {
        return readdir($this->handle);
    }

    /**
     * @return bool
     */
    public function dir_rewinddir()
    {
        rewinddir($this->handle);
        // We do not really have a way to signal a failure as rewinddir() does not
        // have a return value and there is no way to read a directory handler
        // without advancing to the next file.
        return true;
    }

    /**
     * @return bool
     */
    public function mkdir($uri, $mode, $options)
    {
        $this->uri = $uri;
        $recursive = (bool)($options & STREAM_MKDIR_RECURSIVE);
        if ($recursive) {
            // $this->getLocalPath() fails if $uri has multiple levels of directories
            // that do not yet exist.
            $localpath = $this->getDirectoryPath() . '/' . $this->getTarget($uri);
        } else {
            $localpath = $this->getLocalPath($uri);
        }
        if ($options & STREAM_REPORT_ERRORS) {
            return mkdir($localpath, $mode, $recursive);
        } else {
            return @mkdir($localpath, $mode, $recursive);
        }
    }

    /**
     * @return bool
     */
    public function rename($from_uri, $to_uri)
    {
        return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri));
    }

    /**
     * @return bool
     */
    public function rmdir($uri, $options)
    {
        $this->uri = $uri;
        if ($options & STREAM_REPORT_ERRORS) {
            return rmdir($this->getLocalPath());
        } else {
            return @rmdir($this->getLocalPath());
        }
    }

    /**
     * Retrieve the underlying stream resource.
     *
     * This method is called in response to stream_select().
     *
     * @param int $cast_as
     *   Can be STREAM_CAST_FOR_SELECT when stream_select() is calling
     *   stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for
     *   other uses.
     *
     * @return resource|false
     *   The underlying stream resource or FALSE if stream_select() is not
     *   supported.
     *
     * @see stream_select()
     * @see http://php.net/manual/streamwrapper.stream-cast.php
     */
    public function stream_cast($cast_as)
    {
        return $this->handle ? $this->handle : false;
    }

    /**
     * Closes stream.
     */
    public function stream_close()
    {
        return fclose($this->handle);
    }

    /**
     * @return bool
     */
    public function stream_eof()
    {
        return feof($this->handle);
    }

    /**
     * @return bool
     */
    public function stream_flush()
    {
        return fflush($this->handle);
    }

    /**
     * @return bool
     */
    public function stream_lock($operation)
    {
        if (in_array($operation, array(
            LOCK_SH,
            LOCK_EX,
            LOCK_UN,
            LOCK_NB))) {
            return flock($this->handle, $operation);
        }

        return true;
    }

    /**
     * Sets metadata on the stream.
     *
     * @param string $path
     *   A string containing the URI to the file to set metadata on.
     * @param int $option
     *   One of:
     *   - STREAM_META_TOUCH: The method was called in response to touch().
     *   - STREAM_META_OWNER_NAME: The method was called in response to chown()
     *     with string parameter.
     *   - STREAM_META_OWNER: The method was called in response to chown().
     *   - STREAM_META_GROUP_NAME: The method was called in response to chgrp().
     *   - STREAM_META_GROUP: The method was called in response to chgrp().
     *   - STREAM_META_ACCESS: The method was called in response to chmod().
     * @param mixed $value
     *   If option is:
     *   - STREAM_META_TOUCH: Array consisting of two arguments of the touch()
     *     function.
     *   - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner
     *     user/group as string.
     *   - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner
     *     user/group as integer.
     *   - STREAM_META_ACCESS: The argument of the chmod() as integer.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure. If $option is not
     *   implemented, FALSE should be returned.
     *
     * @see http://php.net/manual/streamwrapper.stream-metadata.php
     */
    public function stream_metadata($uri, $option, $value)
    {
        $target = $this->getLocalPath($uri);
        $return = false;
        switch ($option) {
            case STREAM_META_TOUCH:
                if (!empty($value)) {
                    $return = touch($target, $value[0], $value[1]);
                } else {
                    $return = touch($target);
                }
                break;

            case STREAM_META_OWNER_NAME:
            case STREAM_META_OWNER:
                $return = chown($target, $value);
                break;

            case STREAM_META_GROUP_NAME:
            case STREAM_META_GROUP:
                $return = chgrp($target, $value);
                break;

            case STREAM_META_ACCESS:
                $return = chmod($target, $value);
                break;
        }
        if ($return) {
            // For convenience clear the file status cache of the underlying file,
            // since metadata operations are often followed by file status checks.
            clearstatcache(true, $target);
        }
        return $return;
    }

    /**
     * @return bool
     */
    public function stream_open($uri, $mode, $options, &$opened_path)
    {
        $this->uri = $uri;
        $path = $this->getLocalPath();
        $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @
            fopen($path, $mode);

        if ((bool)$this->handle && $options & STREAM_USE_PATH) {
            $opened_path = $path;
        }

        return (bool)$this->handle;
    }

    /**
     * @return string
     */
    public function stream_read($count)
    {
        return fread($this->handle, $count);
    }

    /**
     * Seeks to specific location in a stream.
     *
     * This method is called in response to fseek().
     *
     * The read/write position of the stream should be updated according to the
     * offset and whence.
     *
     * @param int $offset
     *   The byte offset to seek to.
     * @param int $whence
     *   Possible values:
     *   - SEEK_SET: Set position equal to offset bytes.
     *   - SEEK_CUR: Set position to current location plus offset.
     *   - SEEK_END: Set position to end-of-file plus offset.
     *   Defaults to SEEK_SET.
     *
     * @return bool
     *   TRUE if the position was updated, FALSE otherwise.
     *
     * @see http://php.net/manual/streamwrapper.stream-seek.php
     */
    public function stream_seek($offset, $whence = SEEK_SET)
    {
        // fseek returns 0 on success and -1 on a failure.
        // stream_seek   1 on success and  0 on a failure.
        return !fseek($this->handle, $offset, $whence);
    }

    /**
     * Change stream options.
     *
     * This method is called to set options on the stream.
     *
     * @param int $option
     *   One of:
     *   - STREAM_OPTION_BLOCKING: The method was called in response to
     *     stream_set_blocking().
     *   - STREAM_OPTION_READ_TIMEOUT: The method was called in response to
     *     stream_set_timeout().
     *   - STREAM_OPTION_WRITE_BUFFER: The method was called in response to
     *     stream_set_write_buffer().
     * @param int $arg1
     *   If option is:
     *   - STREAM_OPTION_BLOCKING: The requested blocking mode:
     *     - 1 means blocking.
     *     - 0 means not blocking.
     *   - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds.
     *   - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or
     *     STREAM_BUFFER_FULL.
     * @param int $arg2
     *   If option is:
     *   - STREAM_OPTION_BLOCKING: This option is not set.
     *   - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds.
     *   - STREAM_OPTION_WRITE_BUFFER: The requested buffer size.
     *
     * @return bool
     *   TRUE on success, FALSE otherwise. If $option is not implemented, FALSE
     *   should be returned.
     */
    public function stream_set_option($option, $arg1, $arg2)
    {
        trigger_error('stream_set_option() not supported for local file based stream wrappers',
            E_USER_WARNING);
        return false;
    }

    /**
     * @return array
     */
    public function stream_stat()
    {
        return fstat($this->handle);
    }

    /**
     * @return int
     */
    public function stream_tell()
    {
        return ftell($this->handle);
    }
    /**
     * Truncate stream.
     *
     * Will respond to truncation; e.g., through ftruncate().
     *
     * @param int $new_size
     *   The new size.
     *
     * @return bool
     *   TRUE on success, FALSE otherwise.
     */
    public function stream_truncate($new_size)
    {
        return ftruncate($this->handle, $new_size);
    }

    /**
     * @return int
     */
    public function stream_write($data)
    {
        return fwrite($this->handle, $data);
    }

    /**
     * @return bool
     */
    public function unlink($uri)
    {
        $this->uri = $uri;
        return unlink($this->getLocalPath());
    }

    /**
     * @return array
     */
    public function url_stat($uri, $flags)
    {
        $this->uri = $uri;
        $path = $this->getLocalPath();
        // Suppress warnings if requested or if the file or directory does not
        // exist. This is consistent with PHP's plain filesystem stream wrapper.
        if ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) {
            return @stat($path);
        } else {
            return stat($path);
        }
    }


    /****************************************************以上为php流类原型定义的方法,以下为自定义方法*****************************************/
    /**
     * 设置流包装器需要操作的主目录
     */
    public static function setDirectoryPath($dir = '')
    {
        if (empty($dir)) {
            self::$dir = "./";
        }
        if (!is_string($dir)) {
            trigger_error('DirectoryPath must be string', E_USER_WARNING);
        }
        self::$dir = $dir;
    }
    /**
     * 得到流包装器需要操作的主目录
     */
    public function getDirectoryPath()
    {
        self::$dir = empty(trim(self::$dir)) ? "./" : (self::$dir);
        /*
        if (!is_dir(self::$dir)) {
        mkdir(self::$dir, 0777, true);
        }
        //为了方便在实用程序中可以这样做,为标准起见不这样,建立目录不应该是工具库的责任
        */
        return self::$dir;
    }
    /**
     * 得到本地文件路径
     */
    protected function getLocalPath($uri = null)
    {
        if (!isset($uri)) {
            $uri = $this->uri;
        }
        $path = $this->getDirectoryPath() . '/' . $this->getTarget($uri);

        // In PHPUnit tests, the base path for local streams may be a virtual
        // filesystem stream wrapper URI, in which case this local stream acts like
        // a proxy. realpath() is not supported by vfsStream, because a virtual
        // file system does not have a real filepath.
        if (strpos($path, 'vfs://') === 0) {
            return $path;
        }
        $realpath = realpath($path);
        if (!$realpath) {
            // This file does not yet exist.
            $realpath = realpath(dirname($path)) . '/' . basename($path);
        }
        $directory = realpath($this->getDirectoryPath());
        if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) {
            return false;
        }

        return $realpath;
    }

    /**
     * 得到目标
     */
    protected function getTarget($uri = null)
    {
        if (!isset($uri)) {
            $uri = $this->uri;
        }

        list(, $target) = explode('://', $uri, 2);

        // Remove erroneous leading or trailing, forward-slashes and backsl ashes.
        return trim($target, '\/');
    }

}

stream_wrapper_register("yunke", "yunkeWrapper"); //注册yunke流包装器
if (!is_dir("yunke://a/b")) {
    mkdir("yunke://a/b", 0777, true); //创建目录是开发者用户的责任
}
if (file_put_contents("yunke://a/b/wrapperTest.txt", "it is a test by:yunke time20161014") !== false) {
    echo "ok";
} else {
    echo "err";
}

$dir = "yunkenew/subdir"; //再次设定yunke流主目录
yunkeWrapper::setDirectoryPath($dir);
if (!is_dir($dir)) {
    mkdir($dir, 0777, true); //创建yunke流的主目录是开发者用户的责任
}
if (file_put_contents("yunke://wrapperTest.txt", "it is a test by:yunke time20161015") !== false) {
    echo "ok";
} else {
    echo "err";
}

【云游天下 做客四方】 我是云客,欢迎留言,进一步讨论请进群qq群203286137

 

允许转载,请注明出处,尊重分享者的劳动成果

 

 

 

 

Drupal 版本: 

云客Drupal8源码分析之Session进阶

在本系列之前写过《云客Drupal8源码分析之Session系统》,但那部分仅仅讲到了drupal8会话的基础:Symfony的Session组件

至于drupal怎么去使用这个基础就是本主题的内容,本主题是延续篇,将讲述drupal8的全部Session知识

请先看上篇,再继续

关于drupal8的Session代码除了Symfony的Session组件外,全部都放在了:\core\lib\Drupal\Core\Session

在这个文件夹里不仅仅存放了Session的核心代码,还存放了和用户账户相关的一些代码,因为登陆多和Session有关。

drupal8系统的Session子系统是何时初始化并注入到请求对象中的呢?

这个工作是在Drupal\Core\StackMiddleware\Session里完成的,也就是http堆栈中的http_middleware.session层

详情请见本系列前面的《云客Drupal8源码分析之HttpKernel堆栈》

一切从这里开始:$this->container->get("session"); (其实源代码为:$session = $this->container->get($this->sessionServiceName);)

我们来看一看session服务的定义:

使用了类:Symfony\Component\HttpFoundation\Session\Session 有三个参数:

服务:session_manager

属性包:Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag

闪存包:Symfony\Component\HttpFoundation\Session\Flash\FlashBag

(还不知道如何看运行时容器的容器定义数据吗?请见本系列前文的服务容器主题末尾部分)

可见基本使用了Symfony的Session组件,但不同的是使用了不同的SessionStorage 储存管理器

Session处理的核心就是这个储存管理器,它就是本主题的重点,在drupal8的源代码中作者注释到使用SessionStorage

这个名字不是太准确,从它的功能来看,叫做SessionManager更为合适

因此在drupal8中实现SessionStorageInterface接口的SessionStorage被命名为了SessionManager

SessionManager继承自Symfony的NativeSessionStorage,

所以请记住SessionManager就是Symfony概念中的SessionStorage,下文将称为会话管理器。

在会话系统中有三个部分:数据处理器(储存数据包)、会话管理器(负责大部分功能)、会话处理器(负责会话的底层读写)

SessionManager怎么存储数据就看它里面注入的SessionHandler,这个会话处理器就是Drupal\Core\Session\SessionHandler

这个SessionHandler并不直接传递给SessionManager

而是经过层层包装,每一层包装提供一个额外功能,包装情况如下:

SessionHandler:最里层的原始的会话处理器

Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler:第一层包装,提供性能上的优化,如果数据未变避免再次写操作

Drupal\Core\Session\WriteSafeSessionHandler:第二次包装,提供控制是否可写的能力

如果在模块中使用: \Drupal::service('session_handler.write_safe')->setSessionWritable(false);将导致会话无法保存

最后才把经过两次包装的会话处理器传给SessionManager,在服务定义yaml文件里面看不出这种包装关系

其实这个包装关系是在容器编译里面体现的,编译器位于:

Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass

我们看一看SessionHandler,它决定着drupal会话数据怎么储存读写,

看代码可以知道drupal8的Session数据并不是像原生php那样存在文件系统中,

而是放在了数据库里面,数据表名为:sessions,同时储存的还有sessions对应的用户id、客户端主机ip、时间戳、会话哈希id

会话元数据MetadataBag:

MetadataBag用于存放会话本身的属性,drupal的MetadataBag继承了symfonyd MetadataBag,为什么要继承?因为它添加了对跨站请求伪造CSRF的支持

在MetadataBag里面存放了防范CSRF的二次授权种子,称为CsrfTokenSeed,为什么要这个种子请学习CSRF吧

 

现在我们知道了drupal8会话数据的存放,此外会话管理器用到了SessionConfiguration,这比较简单不多讲述。

drupal8会话延迟启动特性:

drupal8具备会话延迟启动功能,这个是drupal为了提高性能额外增加的,在Symfony中并没有

如果在请求中没有发现会话id那么不会启动会话,这个允许匿名缓存发挥作用,匿名缓存系统如果在请求中发现会话id即被认为不是匿名的,将不工作

那我们需要真实的启动怎么办?没有关系,我们其实没有必要这样做,会话对象会在没有启动的情况下会建立$_SESSION

所以我们可以照常使用会话对象,而不会感觉到没有真正启动,如果有会话数据产生那么会话对象在保存的时候会真正启动会话去保存数据

其原理可以使用下面的代码展示:

 

<?php
echo isset($_SESSION)?"isStart":"noStart"; // 并未调用session_start()显示未开始
function yunke()
{
    $_SESSION=array();//变量名$_SESSION具有特殊性,可用超全局,如果圆通$_YUNKE就不行
}
yunke();
echo isset($_SESSION)?"isStart":"noStart"; //超全局会话对象存在了,虽然有$_SESSION超全局变量存在,但会话并没有真正启动,并未调用session_start()
echo session_status (); //返回PHP_SESSION_NONE

在php中表示会话状态有三个常量:

PHP_SESSION_DISABLED 会话功能被禁用。
PHP_SESSION_NONE 会话功能可用,但不存在当前会话(没有启用)。
PHP_SESSION_ACTIVE 会话功能可用,而且存在当前会话(已经启用)。
 

下面我们来看一看\core\lib\Drupal\Core\Session文件夹里面和会话相关的其他代码:

AccountInterface:账户接口,提供基本的账户信息接口

UserSession:默认的账户接口实现

AnonymousUserSession:匿名账户接口

AccountProxyInterface:账户代理接口,其实就是一个账户对象的包装,为什么要设计他?因为可以做账户切换

AccountProxy:账户代理接口的默认实现

AccountSwitcherInterface:账户切换接口

AccountSwitcher:账户切换接口的默认实现

PermissionsHashGeneratorInterface:权限哈希产生器接口

PermissionsHashGenerator:权限哈希产生器

 

以上就是drupal8的基本session全貌了

 

额外信息:

1:在站点配置文件里面可以设置会话的访问更新阀值,也就是多长时间需要更新一次会话数据的最后访问时间

这个话有点绕,其实就是在会话的元数据包里面记录了会话的最后使用时间,源代码:$this->meta[self::UPDATED],这个时间保存在$_SESSION里

开启会话的用户可能连续打开多个页面,每个页面都会访问会话,而会话数据又没有变化,无变化是不会重写会话的

那么每次都去记录一下访问时间,就导致会话$_SESSION改变,需要数据库连接重写会话数据,这样则有些损失性能

那么就设定一个阀值,超过这个阀值时间才记录一次,这样就有比较好的性能,在会话无改变的情况下不用每次都重写数据

格式如下:

$settings['session_write_interval'] =秒数;默认为180秒,

2:默认的会话储存选项是:

    gc_probability: 1 与下一项合用
    gc_divisor: 100    与上面的选项一起决定会话开始时启动垃圾回收进程的概率
    gc_maxlifetime: 200000   寿命超过该值的会话会被清除,在会话处理器中的gc($lifetime)就是用的该值,超期会在数据库中删除

    cookie_lifetime: 2000000 以秒数指定了发送到浏览器的 cookie 的生命周期。值为 0 表示“直到关闭浏览器”。默认为 0

这个默认值可以在/sites/default/services.yml中修改,(对应到自己的站点目录,如果没有services.yml文件请复制一份default.services.yml修改为services.yml)

关于更多会话选项请查看官方手册:http://php.net/manual/zh/session.configuration.php

3:如果你直接从数据库下载会话数据,使用unserialize方法会产生错误,这是由于会话数据的序列化不同于serialize方法,可由session.serialize_handler指定

如果你很好奇想看一看数据库里面保存的会话数据请用以下方法反序列化:

 

session_start();
session_decode(file_get_contents("sessions-session.bin"));
print_r($_SESSION);

4:如何使用drupal8自己的会话对象:

有容器时:$this->container->get("session");

有请求对象时:$request->getSession();

全局使用:\Drupal::service('session');

5:drupal的会话系统和第三方是不兼容的,也就是说如果第三方程序已经开启会话,那么使用drupal会话将报错,请记住必需由drupal来开启会话

也和php.ini 中的自动开始指令 session.auto_start = 1 不兼容,该指令必须设置为0关闭

drupal的会话数据是放数据库的,第三方程序可能不能,而php只能注册一个SessionHandler保存处理器,这是不兼容的根源

这样的不兼容性在Symfony中有介绍,给出了解决办法,请看:http://symfony.com/doc/current/components/http_foundation/session_php_bridge.html

版权声明:本文为云客原创,可转载,但须标明出处,欢迎留言评论,qq群203286137

 

Drupal 版本: 

云客Drupal8源码分析之数据库Schema及创建数据表

本主题是《云客Drupal8源码分析之数据库系统及其使用》的补充,便于查询,所以独立成一个主题

讲解数据库系统如何操作Schema(创建修改数据库、数据表、字段;判断它们的存在性等等),以及模块如何通过一个结构化数组去创建自己用到的数据表

官方的Schema文档地址是:https://www.drupal.org/node/146843
官方API文档:https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%21database.api.php/group/schemaapi/8.2.x
此外在\core\lib\Drupal\Core\Database\database.api.php文件中也有详尽的注释。

数据表定义:

程序要去创建数据表,那么需要先得到一个关于数据表的定义,才能据此创建,在drupal中这个定义是一个嵌套数组,结构如下:

 

$schema = array(
  'description' => 'The base table for nodes.',
  'fields' => array(
    'nid'       => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
    'vid'       => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
    'type'      => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
    'language'  => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
    'title'     => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
    'uid'       => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
    'status'    => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
    'created'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
    'changed'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
    'comment'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
    'promote'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
    'moderate'  => array('type' => 'int', 'not null' => TRUE,'default' => 0),
    'sticky'    => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
    'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
  ),
  'indexes' => array(
    'node_changed'        => array('changed'),
    'node_created'        => array('created'),
    'node_moderate'       => array('moderate'),
    'node_frontpage'      => array('promote', 'status', 'sticky', 'created'),
    'node_status_type'    => array('status', 'type', 'nid'),
    'node_title_type'     => array('title', array('type', 4)),
    'node_type'           => array(array('type', 4)),
    'uid'                 => array('uid'),
    'translate'           => array('translate'),
  ),
  'unique keys' => array(
    'vid' => array('vid'),
  ),
  // For documentation purposes only; foreign keys are not created in the
  // database.
  'foreign keys' => array(
    'node_revision' => array(
      'table' => 'node_field_revision',
      'columns' => array('vid' => 'vid'),
     ),
    'node_author' => array(
      'table' => 'users',
      'columns' => array('uid' => 'uid'),
     ),
   ),
  'primary key' => array('nid'),
);

下面解释各个键的意思:

 

 

      'description': 一个非标记字符串,用来描述表的目的.引用到其他表时,其他表名用{}包裹,如"储存标题数据为表{node}."
    'fields': 一个关联数组('字段名' => 规格描述) 它描述了表的列,规格描述也是一个数组. 以下规格参数被定义:

        'description': 一个非标记字符串,用来描述字段的目的.引用到其他表时,其他表名用{}包裹,如"储存标题数据为表{node}."
        'type': 常用数据类型: 'char', 'varchar', 'text', 'blob', 'int', 'float', 'numeric', or 'serial'. 大多数类型映射到相应数据库引擎指定的类型.  'serial' 代表自增字段. 对于MySQL来说是 'INT auto_increment'. 特殊类型'varchar_ascii'可用来限制字段机器名为US ASCII字符集.
        'mysql_type', 'pgsql_type', 'sqlite_type'等等:如果你要的类型在上面没有,你可以为特殊数据库指定类型,这时可以省略上面的类型参数,但这样可能有风险,通常可以使用"text"来代替
        'serialize': 布尔值,指定字段是否应该存储序列化数据
        'size': 数据大小: 'tiny', 'small', 'medium', 'normal', 'big'. 这是一个可储存最大值的暗示,指导储存引擎使用何种类型(比如在 MySQL中有 TINYINT、INT 、 BIGINT). 缺省值是'normal'(在MySQL中是INT, VARCHAR, BLOB,). 对于各种数据类型而言,不是所有大小均可用,可能的组合见DatabaseSchema::getFieldTypeMap()
        'not null': 布尔值,true表示no NULL值.默认为false.
        'default': 字段的默认值,注意数据类型,在php中'', '0',  0 是不同的. 为'int'字段指定'0'将不工作(注意引号),因为'0'是字符串,不是整数0
        'length': 类型'char', 'varchar' or 'text' 字段的最大长度.对于其他字段类型将忽略此值
        'unsigned': 布尔值,指明'int', 'float' and 'numeric' 类型有无符号(正负).默认为FALSE.对于其他字段类型将忽略此值
        'precision', 'scale': 对于'numeric' 字段而言是必须的,precision指明精度(有效数字的总数),scale (小数位位数). 其他类型将忽略
        'binary': 布尔值,指明 MySQL应该强制 'char', 'varchar' or 'text' 字段类型使用大小写敏感的二进制校对. 对于默认就是大敏感的类型,它没有影响

    字段定义中除'type'以外的全部参数是可选的,但有些另外: 'numeric' 必须指定'precision' 和'scale', 'varchar' 必须指定'length'

    'primary key': 构成主键的多个列数组,一个表中只能有一个主键,但一个主键可以包含多个列,称为联合主键。
    'unique keys': unique键的关联数组 ('keyname' => specification). specification是一个构成unique的列数组
    'foreign keys': 一个关联数组('my_relation' => specification). 每个定义是一个包含被引用表名及列映射的数组,列映射为('源列' => '引用列'),外键不在数据库中创建,drupal不强制这个
    'indexes': 索引的关联数组 ('indexname' => specification). 

上面的定义有些额外的说明:
primary key:主键,一个表中只能有一个主键约束,你可以命名这个主键约束,并指定它包含有哪些列,比如一个学生表,可以指定主键约束包含班级和学号,那么在行中班级可以相同,学号也可以相同,但班级和学号不能同时相同,这就是联合主键的作用

unique keys:唯一性,对字段数据进行唯一性约束,可以同时指定多个字段,被指定的这些字段,它们各自的数据不可以重复,各行数据在这个字段必须是唯一的

foreign keys:外键,这指明本字段引用到其他表的某个字段,在本字段所储存的值必须是被引用到的那个字段储存的值之一,如果插入值时,不在被引用字段内容中,那么将产生错误,比如权限表要储存用户ID,那么这个字段就应该指定外键到用户表的id字段

indexes:索引,主要用于加速数据查询,提高性能,包含多个列的索引又叫复合索引或者多列索引,条件子句涉及到索引指定的列时,在数据库内部这些索引被用到,当表很大时能大大提高搜索速度,表较小则索引体现不出优势,复合索引指定列时,顺序不是随意的,关于这方面知识请看:http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html

在上面的定义中:索引处有个定义是'node_type' => array(array('type', 4)),为什么不是'node_type' => array('type',),呢?这个参数4是什么意思?这是指在type这个字段中储存的数据只有前4字节被用于索引内容,加快速度,如果存储引擎不支持这样的功能,那参数4将被忽略,这样的用法同样适用于主键。

 

有了这样的表定义就可以创建表了,方法如下:

 

 

$con=\Drupal\Core\Database\Database::getConnection($target, $key);
$con->schema()->createTable($table_name, $schema);

$con->schema()返回的是一个schema对象,专门操作schema,里面提供了许多方法
源码在\core\lib\Drupal\Core\Database\Driver\中,它是\core\lib\Drupal\Core\Database\Schema.php的子类,

母类提供个数据库通用功能,子类用以解决数据库之间的不同(数据库方言)

Schema对象的常用方法如下:

 

$con = \Drupal\Core\Database\Database::getConnection($target, $key);
$schema = $con->schema();
$schema->createTable($table_name, $schema); //创建表
$schema->tableExists($table); //判断表是否存在
$schema->renameTable($table, $new_name);//重命名表
$schema->fieldExists($table, $column); //判断字段是否存在
$schema->fieldNames($fields);//从一个column specifiers返回字段名
$schema->changeField($table, $field, $field_new, $spec, $keys_new = array());//修改字段
$schema->addField($table, $field, $spec, $keys_new = array());//添加字段
$schema->dropField($table, $field);//删除字段
$schema->fieldSetDefault($table, $field, $default);//为字段设置默认值
$schema->fieldSetNoDefault($table, $field); //取消默认值
$schema->indexExists($table, $name); //判断是否存在索引
$schema->addIndex($table, $name, $fields, $spec = array());//添加索引
$schema->dropIndex($table, $name);//删除索引
$schema->addPrimaryKey($table, $fields); //给联合主键添加字段
$schema->dropPrimaryKey($table);//删除主键
$schema->addUniqueKey($table, $name, $fields);//添加唯一性约束
$schema->dropUniqueKey($table, $name);//销毁唯一性约束
$schema->prepareComment($comment, $length = null); //预处理注释
$schema->getComment($table, $column = null); //获取字段注释

这些方法的说明请看注释:\core\lib\Drupal\Core\Database\Schema.php

 

常用数据库Mysql数据库的类型映射如下:

 

$map = array(
        'varchar_ascii:normal' => 'VARCHAR',

        'varchar:normal' => 'VARCHAR',
        'char:normal' => 'CHAR',

        'text:tiny' => 'TINYTEXT',
        'text:small' => 'TINYTEXT',
        'text:medium' => 'MEDIUMTEXT',
        'text:big' => 'LONGTEXT',
        'text:normal' => 'TEXT',

        'serial:tiny' => 'TINYINT',
        'serial:small' => 'SMALLINT',
        'serial:medium' => 'MEDIUMINT',
        'serial:big' => 'BIGINT',
        'serial:normal' => 'INT',

        'int:tiny' => 'TINYINT',
        'int:small' => 'SMALLINT',
        'int:medium' => 'MEDIUMINT',
        'int:big' => 'BIGINT',
        'int:normal' => 'INT',

        'float:tiny' => 'FLOAT',
        'float:small' => 'FLOAT',
        'float:medium' => 'FLOAT',
        'float:big' => 'DOUBLE',
        'float:normal' => 'FLOAT',

        'numeric:normal' => 'DECIMAL',

        'blob:big' => 'LONGBLOB',
        'blob:normal' => 'BLOB',
        );

 

 

在模块安装的时候怎么创建数据表呢?

在模块文件夹下创建一个文件,命名为:module_name.install

module_name是模块名,把此文件当做php文件看待,在里面定义函数hook_schema().

其中hook应该替换为模块名,比如模块名叫yunke,该函数名应该为yunke_schema()。

在函数中返回一个关联数组,键名表示数据库表名,键值为数据表定义数组(见上),格式如下:

 

function yunke_schema() {
  $table['table_name1'] = $schema;
  $table['table_name2'] = $schema2;
  return $table;
}

table_name1和table_name2为要创建的表名,他们的值为上面定义的表定义数组

 

在模块安装的时候将搜索module_name.install文件,执行里面的hook_schema()函数
通过返回值自动建立这两个数据表,结构为定义数组控制,在模块卸载的时候自动销毁这两个表

模块升级可能会涉及到数据表的更新、变更等升级操作,
这应该是一个独立的主题(它包含表定义、配置、数据处理等等),此处不做介绍,在模块相关主题中讲解

 

 

我是云客【云游天下,做客四方】

原创内容,欢迎转载,但须注明出处,欢迎留言评论,讨论请加qq群203286137

 

Drupal 版本: 

云客Drupal8源码分析之数据库系统及其使用

在开始本主题前请允许一点点题外话:

在我写这个博客的时候(2016年10月28日),《Begining Drupal 8》这本书已经翻译完成并做成了PDF格式供给大家免费下载,这是一本引导新人学习drupal8的入门级教程,由drupal中文社区站http://drupalchina.cn/的站长龙马组织翻译,有20位奉献者进行了大半年的工作得以完成,很荣幸我也是其中之一,用以进行这项工作的qq群号是:342823468,在这个群里诞生了第一本drupal8中文教程,这件事真的很赞!群里的20位翻译者真的很赞!目前国内没有一个由社区开发的php内容管理系统,而建立一个社区cms对大众又是多么有益,drupal在国际上如此流行,众人聚焦精力对它精雕细琢造就了不错的品质,延展使用范围,快速迭代,以至于许多知名机构和公司用它做官网,而在国内尽管发展速度还不错,但中文资料匮乏和缺乏系统整理严重影响了很多新人的步伐,这也是20位翻译者无偿劳动的意义所在,希望国内社区越来越大,这样大家都有益处,一个人是创作不了LINUX那样的伟业的,人多才能有生态,有生态才能反哺大家,这也是我写云客drupal8源码分析的一个愿望,希望越来越多人加入这个社区。

好了,下面开始本篇的主题,主要讲解drupal8数据库系统的实现和使用方面的知识:

Symfony没有数据库组件,drupal8完全自己实现了一个基于php的pdo扩展的数据库系统,它提供了一个数据库抽象层,让你可以使用统一的方式去操作数据库,而不用管底层使用的是什么数据库,只需要使用好它提供的接口(对象方法或函数)就行,当需要更换另外类型的数据库时,比如由MySQL换成Oracle或MS SQL Server,无需修改应用层代码。在发布版中默认提供了mysql、pgsql、sqlite支持,
它也提供多台数据库服务器支持,简单的负载均衡很容易实现,在学习它之前建议你对以下内容有所了解:

1:php的PDO扩展,官方地址是:http://php.net/manual/zh/intro.pdo.php
这是php层面提供的数据库抽象层,用以统一各类数据库的操作,drupal8的数据库系统是基于它实现的,了解它后在学习的过程中就可以清楚知道什么事情是谁做的以及它们是怎么配合的

2:SQL标准
SQL99也叫作SQL:1999、SQL3 、 SQL-99. 是sql语句的标准https://en.wikipedia.org/wiki/SQL:1999,还有ANSI SQL:2003,他们定义SQL语句的标准和数据库应该具有的行为,了解这以后在看drupal8数据库的sql语句构建时就不会迷惑了

3:软件设计模式
在drupal8的设计中到处是模式,数据库系统中装饰者模式尤为突出,了解这后看代码会轻车熟路,会有很熟悉轻松的感觉,但不建议买大部头的书看,网上很多帖子足以,几个列子就能让你明白,节省时间

drupal官方的数据库文档:https://www.drupal.org/developing/api/database,注意D7和D8版本大同小异,看完本文有不清楚的请到那里补充。

下面先看一看数据库连接信息的定义:

定义位于站点配置文件中/sites/default/settings.php,你可以看到类似下面的定义:

$databases['default']['default'] = array (
  'database' => 'drupal',
  'username' => 'yunke',
  'password' => 'yunke',
  'prefix' => 'yunke_',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

$databases['default']['default']的值是一个数组,这个数组称之为DSN(数据源名称Data Source Name),它代表一个数据库,包含连接所需的所有信息

为叙述简单后面我们将这个数组表示成$dsn

像上面的代码:$databases['default']['default']=$dsn;可能会让你疑惑,为什么是嵌套数组?有什么含义?

这就是drupal提供多数据库支持的体现,以下我们用$databases[$key][$target]=$dsn;来表示

$key指一个数据库,代表一个单独系统,不同的$key通常是不同结构的数据库,当和drupal以外的第三方系统协作时其他系统的数据库就应该是不同的$key

默认的drupal只有一个$key,它被命名为default,它是drupal使用的数据库

$target是什么呢?它主要用于负载均衡,一个$key对应的数据库,他们有相同的结构和数据,可以被分布在多台服务器上面,以减轻压力

那么每一台服务器就对应一个$target,比如MySQL数据库内建了一个主从备份的功能,在一个服务器上面的MySQL实例可以快速同步到其他服务器

那么就可以有多个服务器拥有相同的数据库结构和数据,在非写入查询的时候能分摊访问实现负载均衡,但写入更新一类的查询只能在主服务器上面运行

(关于Mysql数据库的主从复制推荐大家看《高性能MySQL》一书Baron Schwartz,Peter Zaitsev,Vadim Tkachenko 著;中文有售)

明白$databases['default']['default']的意思了吧,这也是大多数小型网站所需要的配置

表示有一个叫做default的数据库,这个数据库只有一个叫做default目标实例的服务器在运行,所有读写操作都在这个服务器上面

那么怎么配置多数据库呢?可以这样:

$databases['default']['default']=$dsn_1; //主服务器
$databases['default']['replica']=$dsn_2; //用于只读的从服务器

上面实现了两台服务器,那么要实现多个从服务器该怎么操作?可以这样:

$databases['default']['default']=$dsn_1; //主服务器
$databases['default']['replica']=$dsn_2; //用于只读的从服务器
$databases['default']['yunke']=$dsn_3; //用于只读的从服务器

这样虽然可以,但是每次获得链接时都要指定$target很不方便,

况且同一个$key下面只有主服务器可以写,多个$target从服务器是一样的,且都是只读,所以基本使用下面的方式:

$databases['default']['default']=$dsn_1; //主服务器
$databases['default']['replica'][]=$dsn_2; //用于只读的从服务器
$databases['default']['replica'][]=$dsn_3; //用于只读的从服务器
$databases['default']['replica'][]=$dsn_4; //用于只读的从服务器

drupal在一个$target下有多个$dsn时,它会随机选择一个,就实现了负载分摊,如无特别需要基本使用以上方式

注意:在一个$key下必须要有一个$target被命名为default,且它作为主服务器,在程序内部当其他$target不可用时默认回退到default

关于写查询的负载均衡比较复杂,比如数据分片技术,需要应用层规划,定义额外的键,请看上面推荐的书

明白了$key和$target后我们看一看$dsn这个数组是怎么定义的:

$dsn= array (
  'database' => 'drupal',
  'username' => 'yunke',
  'password' => 'yunke',
  'prefix' => 'yunke_',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

以上是最基本的信息,大部分你应该能看懂,主要讲以下内容:

表前缀只有一个字符串值的时候,表示所有数据表均使用该前缀,不用请为空或者省略该数组元素

很赞的是drupal支持为不同表运用不同前缀,使用如下:

'prefix' => array("default"=>"默认前缀","基本表名"=>"特定前缀","基本表名"=>"特定前缀"),

在程序内部就被转化成这样的格式,其中default必不可少,用于指定没有特定前缀的表默认使用的前缀,不需要请='',

指定名字空间:'namespace'=> 'Drupal\\Core\\Database\\Driver\\mysql',

这个表示drupal数据库抽象层使用特定数据库驱动程序的名字空间,不加也可以,但最好加上,可提高程序速度,避免自动通过反射机制得到

在$dsn中还可以指定其他配置项,通常不同数据库有不同配置,但也有一些通用配置,下面说说常用的MySQL可使用的配置项:

$dns['_dsn_utf8_fallback'] = TRUE
drupal默认使用utf8mb4数据库字符编码,它是utf8的超集,此指令表示回退到utf8编码,不使用utf8mb4
默认使用utf8mb4,如果已经使用utf8mb4后不能回退到低版本字符编码的数据库,这样会丢失超集部分的数据
详见:http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
$dns['unix_socket']使用一个unix_socket
$dns['pdo']允许的pdo选项,用于控制pdo的行为
$dns['collation']运行设置mysql的字符集语句时:SET NAMES ' . $charset . ' COLLATE ' . $dns['collation'],没有这个指令则设置:'SET NAMES ' . $charset
$dns['init_commands']执行mysql的初始化命令
$dns['transactions']表示是否支持事务,没有设置则默认支持事务,除非明确设置为false以使强制不支持事务

 

以上就是关于如何定义数据库连接选项的内容,下面说说drupal允许使用的数据库命名规则:

drupal使用的数据库名、表名、字段名必须是字母、数字、下划线和点号
判定规则是:preg_replace('/[^A-Za-z0-9_.]+/', '', $database);
他们的别名不能有点号(别名就是查询语句as后面的名字),规则是preg_replace('/[^A-Za-z0-9_]+/', '', $field);

 

接下来看看怎么进行数据库查询:

在整个drupal程序运行开始阶段就把数据库的配置信息注入到了数据库控制类中,由DrupalKernel完成调用
具体是在Drupal\Core\Site\Settings 的 initialize方法调用Database::setMultipleConnectionInfo($databases);

当需要查询的时候首先需要获得数据库连接类,在模块中你可以这样操作:

\Drupal::database(); //获取配置中$databases['default']['default']表示的链接,这对于大多数只有一个数据库的站点而言是最常用的,全局获取
\Drupal::service("database"); //完全等同于\Drupal::database();
$container()->get("database"); //效果同上,在容器对象可用时使用
\Drupal::service("database.replica"); //获取配置中$databases['default']['replica']表示的备用数据库链接,无设置将回退到主库
$container()->get("database.replica"); //效果同上,在容器对象可用时使用

以上是常用的快捷方法,更加灵活的方法是:

\Drupal\Core\Database\Database::getConnection($target, $key); //这样可以指定任意目标数据库

在得到链接对象后就可以使用它做查询了,在官方文档中查询分为两大类:静态查询、动态查询

其实这个名字有些迷惑人,所谓静态查询就是直接写SQL语句查询,而动态查询是调用系统提供的语句构建方法逐步构建SQL语句再查询

先看一看所谓的静态查询,也就是直接写SQL语句的查询怎么做:

$con=\Drupal::database();
$con->query("SELECT nid, title FROM {node} WHERE type = :type", array(':type' => 'page'));
$con->query("SELECT * FROM {node} WHERE nid IN (:nids[])", array(':nids[]' => array(13, 42, 144)));

在上面的SQL中表名均是被放在花括号里面,是不带前缀的表名,系统依据这样的语法自动添加表前缀,无{}不会添加前缀
花括号里面不能有空格,两侧需要留至少一个空格,在SQL语句中其他部分不能使用{},仅仅被表名使用
也看到可以使用参数传递,参数占位符名以冒号开始不加引号,在关联数组中元素顺序没有关系,这样的设计系统自动防止注入攻击,无需转义参数值
drupal8可以让我们使用数组方式的占位符,看上面最后一条语句,这在PDO中是不允许的,drupal会为我们自动展开数组

静态查询虽然直截了当,但有两个缺点:

第一:它无法让模块查询钩子对SQL语句进行修改

第二:直接写的SQL语句可能不会兼容所有的数据库,那么在更换数据库类型的时候带来麻烦

所以基于上面的原因,推荐使用动态查询,drupal为每一种类型的查询都准备了一个类,它有许多方法来帮助构建查询语句,比如:

$con=\Drupal::database();
$query = $con->select('users', 'u')
  ->condition('u.uid', 0, '<>')
  ->fields('u', array('uid', 'name', 'status', 'created', 'access'))
  ->range(0, 50);
$result = $query->execute();
foreach ($result as $record) {
  // Do something with each $record
}

动态查询类似于CI框架的活动记录类,它不需要懂得SQL怎么写,根据提供的方法构建即可

在这个例子中$con->select()方法返回一个select查询构建对象,这个对象采用链式调用自己的方法帮助最终产生一个查询sql

如果方法返回的是对象本身则可使用链式调用,数据库连接对象可以返回多种查询构建对象,他们有不同的帮助方法,如下:

$con->select($table, $alias = NULL, array $options = array()); //构建查询对象
$con->insert($table, array $options = array()); //构建插入SQL语句对象
$con->merge($table, array $options = array()); //构建合并查询
$con->upsert($table, array $options = array()); //构建upset查询对象,数据库不支持则模拟
$con->update($table, array $options = array()); //构建更新查询对象
$con->delete($table, array $options = array());  //构建删除查询对象
$con->truncate($table, array $options = array());  //构建清空数据表查询对象

这些查询构建对象的定义在这里:\core\lib\Drupal\Core\Database\Driver\,
 它们继承自母类\core\lib\Drupal\Core\Database\Query,母类提供各数据库相同功能,子类解决不同数据库的特殊性
这些查询构建类各自定义了很多方法去构建自己类型的sql查询,这些sql语句是满足数据库类型的,
上面的这些查询构建对象都可以通过强制类型转换(string)$var来得到构建的SQL语句,在他们内部用php魔术方法__toString()来实现此目的,实际上最终就是使用该方法得到SQL语句并传递给Connection连接对象的中心查询方法去执行。

要把每个构建对象及他们的构建方法介绍完,需要很大篇幅,这里不做介绍,你可以到官网文档查看,如需深入理解直接看类定义代码吧,它有详细的注释,官网的API文档就提取自这些注释。

动态查询除了帮助构建兼容的SQL外还可以添加查询标签,这个功能可以让drupal模块据此修改相应的查询SQL

创建数据库及表结构:

要创建一个数据库,以及定义里面的表结构,在drupal中往往不是通过静态查询功能实现的,drupal数据库链接对象提供了一个方法来返回schema对象:

schema对象操作数据库结构定义,这是一块很重要的内容,由于篇幅有限,将在下一篇源码分析中专门介绍,基本使用如下:

$con=\Drupal::database();
$con->schema();

开启数据库事务:

数据库事务让查询具备原子性、一致性,在drupal中默认是支持事务的,除非在链接选项中明确禁止事务,mysql默认使用支持事务处理的InnoDB储存引擎

$con=\Drupal::database();
$yunke=$con->startTransaction($name = ''); //开始事务,参数指回滚点,只要变量$yunke不被销毁那么事务持续开启,一旦销毁即被提交
$con->rollback($savepoint_name = 'drupal_transaction')  //回滚事务到某个回滚点
$yunke=NULL;//变量被销毁,事务被提交

不要使用$con->commit();去提交一个事务,这会抛出异常,系统会隐式自动提交,
需要注意的是DDL语句(数据库定义语句)在大多数数据库中是不支持事务的,包括MYSQL

使用结果集:

SELECT查询返回一个或多个数据,这些结果集被包装在Drupal\Core\Database\Statement中,它继承自PDO的PDOStatement类,可以使用它的全部方法,行为受到查询选项的控制,通常使用foreach循环去获取结果,如下:

 

$con=\Drupal::database();
$result = $con->query("SELECT nid, title FROM {node}");
foreach ($result as $record) {
  // Do something with each $record
  $node = node_load($record->nid);
}
$record = $result->fetch();            // 使用默认fetch模式获取下一行数据.
$record = $result->fetchObject();  // 以stdClass对象形式取回
$record = $result->fetchAssoc(); //以数组方式取回
$record = $result->fetchField($column_index); //获取一行中的一个字段,$column_index是列索引值,以0开始
$number_of_rows = $result->rowCount(); //计算 DELETE、INSERT 、UPDATE 影响的行数,SELECT不应该使用这个方法
$con->select('users')->countQuery()->execute()->fetchField(); //SELECT应该使用这个方法,在countQuery()调用前需要构建好查询

更多结果集的操作请查看:https://www.drupal.org/docs/7/api/database-api/result-sets

 

数据库操作的过程式包装:

php的函数是全局可用的,为了方便操作,drupal把许多数据库操作功能封装到函数中,这样就可以在任何地方调用了

这些函数定义在/core/includes/database.inc和/core/includes/schema.inc中,它们在HTTP核心堆栈的预处理层被载入

你可以在模块中直接使用,详情见这两个文件

上面基本讲到使用层面的内容,下面我们看看源码布局:

在drupal中数据库源码位于:\core\lib\Drupal\Core\Database,
其中Driver子目录存放不同数据库的驱动,解决差异问题(数据库方言),Query子目录存放SQL查询语句构建类,专门构建SQL语句,所有的执行汇总到链接类Connection的query方法上,而此方法还不是真正执行查询的方法,查看源码:

 

$stmt = $this->prepareQuery($query);
$stmt->execute($args, $options);

这个$stmt其实是Drupal\Core\Database\Statement对象,为什么是这个?在链接对象构造函数中有:

 

 

$connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));

这个$this->statementClass就指定了Drupal\Core\Database\Statement,这个设置就是让pdo返回这个对象
如果还不明白,请看:http://php.net/manual/zh/pdo.setattribute.php

 

所以最终执行drupal所有查询的是Drupal\Core\Database\Statement的execute方法

查询日志记录就设置在这个方法里面,如果需要开启查询日志记录可以这样:

 

\Drupal\Core\Database\Database::startLog($logging_key, $key = 'default'); 
//开始日志记录 配置中的一个$key对应一个日志记录器,$logging_key可以是$target也可以自己随意指定
\Drupal\Core\Database\Database::getLog($logging_key, $key = 'default');  //得到查询日志

查询日志是一个数组,内容如下:

 

 

array(
        'query' => "查询语句",
        'args' => "参数",
        'target' => $target,
        'caller' => "调用者",
        'time' => "查询这条语句执行的时间",
      );

在调试的时候使用它非常方便,可以用drupal的日志系统去储存这个日志
 

 

如何将drupal的数据库系统提取出来?

如果你觉得drupal的数据库系统很好,想提取出来用在自己其他的项目上面,你将需要处理下面的工作:

1:drupal数据库代码在数据库中建立了一个sequences表,用于满足nextId()的功能,提供一个比之前返回的数大的唯一整数
2:在durpal中模块可以修改查询,数据库代码对查询提供标签管理并触发模块修改,在preExecute方法中调用
\Drupal::moduleHandler()->alter($hooks, $query);修改查询语句,模块通过这些标签修改查询,

处理好这些和drupal系统紧密耦合的地方就可以提取出来单独使用了

 

以上就是drupal8数据库代码的所有知识,不尽之处相信已经可以轻松通过源代码或官方文档查清楚了
下一篇将介绍数据库的Schema,它是一个API,介绍如何在drupal的应用层定义数据库,如何建库建表

我是云客【云游天下,做客四方】

原创内容,欢迎转载,但须注明出处,欢迎留言评论,讨论请加qq群203286137

 

Drupal 版本: 

云客Drupal8源码分析之控制器执行及其解析器controller_resolver

在drupal的HttpKernel核心中使用控制器解析器来取得要执行的控制器,以及解析出控制器需要的参数
除此之外也在多个地方用到它,比如渲染数组的回调解析,是一个重点内容

它的服务ID为:controller_resolver,接受以下两个参数:

psr7.http_message_factory:用于创建psr7描述的请求对象(关于这个请看:http://www.php-fig.org/psr/psr-7/
class_resolver:从容器里面取服务

下面来看一看它是怎么工作的:
它需要实现ControllerResolverInterface接口,里面有两个方法定义:
public function getController(Request $request); //返回一个回调作为控制器
public function getArguments(Request $request, $controller); //求出控制器的参数,并以索引数组的方式返回
这个是Symfony的接口定义,此外drupal加了一个方法:
public function getControllerFromDefinition($controller); //以路由中对控制器的定义来获取控制器,同样返回回调

所以drupal的控制器解析器一共要实现接口的以上三个方法。

先看一看它怎么解析出控制器:
这必须依赖于路由过程中在请求对象属性包上设置的_controller参数,代码如下:
$request->attributes->get('_controller')),解析就是基于_controller的内容。
所有经过路由处理的请求对象上一定会有_controller变量,它来自路由定义的defaults字段
你可能奇怪在有些路由定义中没有定义_controller呢?
没有定义_controller的路由会在路由增强器里面定义,比如处理表单的路由里面定义了_form,而没有定义_controller,
但在增强器FormRouteEnhancer中就定义了$defaults['_controller'] = 'controller.form:getContentResult';
其中:controller.form表示服务ID,实体也是类似情况,这个用法见下文
当流程到达这里时,总之一定有_controller这个变量设置在请求属性包中。

_controller的定义:它可以是函数名或者具备__invoke方法的对象,也可以只是一个类,但它必须包含__invoke方法
类名必须是全限定的名字空间类名,控制器设置不能使用路径名,系统会通过类加载器找到控制器
大多数情况是用:或者::分割的字符串,它表示某个类的某方法,在这里:和::没有区别
这个分隔符还有个特殊的用法,前面部分不一定是类名也可以是容器的服务名,所以可以执行容器的服务方法

这个特殊用法是在class_resolver服务中实现的,上文已经说了它作为参数传递给控制器解析器。
类定义在\core\lib\Drupal\Core\DependencyInjection\ClassResolver.php
这里需要注意的是在:或者::之前的部分如果是一个类,并且它实现了以下接口:
Drupal\Core\DependencyInjection\ContainerInjectionInterface
Symfony\Component\DependencyInjection\ContainerAwareInterface
那么它会被实例化并自动注入服务容器,在drupal中几乎全部服务都在容器中,
ControllerBase实现了ContainerInjectionInterface,它就是此时自动注入容器,所以能提供那些常用的服务
明白为什么控制器继承Drupal\Core\Controller\ControllerBase就会有容器了吧!

以上就是获得控制器的过程,经过控制器解析器处理返回了一个回调
紧接着核心在得到回调后会派发kernel.controller事件,系统默认提供了两个侦听器:

服务id:path_subscriber 方法:onKernelController 作用是让别名管理器设置缓存键
服务id:early_rendering_controller_wrapper_subscriber 方法:onController 作用如下:

这个侦听器特别重要,但也让人感觉特别别扭,在控制器执行的时候要渲染页面本应该返回渲染数组
但可能也会在控制器中直接调用drupal_render()来渲染,这样过早的渲染称为:"early rendering"
这样做会让系统侦测不到渲染上下文,丢失一些元数据,影响到缓存等等,所以drupal8使用这个侦听器来解决这个问题
(过早渲染在drupa9中会被移除,禁止使用),那么它是怎么解决的呢?

其实所有的控制器都被这个侦听器包装在了一个闭包里面,在闭包中解决,Symfony HttpKernel得到的控制器全是经过包装的
真正的参数解析工作也在这里进行,在\Symfony\Component\HttpKernel\HttpKernel::handleRaw()中进行的参数解析纯属浪费资源,但它毕竟是第三方库,代码不受drupal控制,白白浪费计算资源了,读者可以试试在handleRaw()中参数解析后面重置参数为空数组,系统运行照样是没有任何问题的,这里建议drupal8的控制器解析器要设置缓存机制,避免这种浪费,参数解析的反射操作是很耗资源的。

由于这个侦听器的特殊作用,当我们在注册kernel.controller事件侦听器时一定要注意优先级,此侦听器优先级为默认的0

下面来看一看控制器解析器是怎么获取参数的,一句话总结:使用php的反射机制。

许多drupal的初学者应该都好奇过控制器的参数问题吧,核心代码如下:

 

 protected function doGetArguments(Request $request, $controller, array $parameters) {
    $attributes = $request->attributes->all();
    $raw_parameters = $request->attributes->has('_raw_variables') ? $request->attributes->get('_raw_variables') : [];
    $arguments = array();
    foreach ($parameters as $param) {
      if (array_key_exists($param->name, $attributes)) {
        $arguments[] = $attributes[$param->name];
      }
      elseif (array_key_exists($param->name, $raw_parameters)) {
        $arguments[] = $attributes[$param->name]; //在8.2.3及之前的版本中,这里是一个bug,此处应该是$raw_parameters[$param->name],笔者已经提交了bug报告
      }
      elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
        $arguments[] = $request;
      }
      elseif ($param->getClass() && $param->getClass()->name === ServerRequestInterface::class) {
        $arguments[] = $this->httpMessageFactory->createRequest($request);
      }
      elseif ($param->getClass() && ($param->getClass()->name == RouteMatchInterface::class || is_subclass_of($param->getClass()->name, RouteMatchInterface::class))) {
        $arguments[] = RouteMatch::createFromRequest($request);
      }
      elseif ($param->isDefaultValueAvailable()) {
        $arguments[] = $param->getDefaultValue();
      }
      else {
        if (is_array($controller)) {
          $repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]);
        }
        elseif (is_object($controller)) {
          $repr = get_class($controller);
        }
        else {
          $repr = $controller;
        }

        throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name));
      }
    }
    return $arguments;
  }

参数:$parameters是一个ReflectionParameter对象组成的数组,见:http://php.net/manual/zh/class.reflectionparameter.php
它来自以下方法中:

 

 

   public function getArguments(Request $request, $controller)
    {
        if (is_array($controller)) {
            $r = new \ReflectionMethod($controller[0], $controller[1]);
        } elseif (is_object($controller) && !$controller instanceof \Closure) {
            $r = new \ReflectionObject($controller);
            $r = $r->getMethod('__invoke');
        } else {
            $r = new \ReflectionFunction($controller);
        }

        return $this->doGetArguments($request, $controller, $r->getParameters());
    }

从以上解析过程中可以知道控制器参数采用的顺序、命名规则、类型暗示:

 

先从$request->attributes找同名参数,这也是为什么路由定义中额外参数能作为控制器参数的原因,路由定义的占位符名要和控制器参数名一致
再从$request->attributes中的_raw_variables找同名参数,它是未经过转换的原始变量
再看参数是不是有类型暗示为请求对象,如果是则将请求作为参数
再看是不是psr7描述的请求类型,它实现ServerRequestInterface接口,如果是则转化请求并传递
再看类型暗示是不是路由匹配器(RouteMatchInterface或其子类),如果是则传递路由匹配器
最后传递参数的默认值,如果不能找到参数将报错

以上就是控制器解析器的全部内容了,以下是一些补充:

1:在drupal8中所有控制器是放在闭包里面执行的,这样做是避免过早渲染带来的问题,此特性在D9中移除
2:控制器最好定义默认值,否则当找不到值时会报错
3:控制器最好继承Drupal\Core\Controller\ControllerBase,这样比较方便使用
4:请求对象及路由匹配器可以在参数中获得也可以使用容器获得

 

我是云客,【云游天下,做客四方】欢迎转载,但须注明出处,讨论请加qq群203286137

 

 

 

 

Drupal 版本: 

云客Drupal8源码分析之页面标题

本篇主题讲解drupal8系统是如何计算页面标题的,标题很重要,尤其对于搜索引擎优化来说,标题权重很高

页面有标题当然是针对请求格式为html而言,在整个执行流程中如果控制器直接返回响应对象,那么标题计算就在控制器中随意进行
流程仅仅停留在Symfony的渲染管道中,如果控制器返回的是渲染数组,那么将派发视图事件,主内容视图订阅器MainContentViewSubscriber将判断请求格式,并启动对应的格式渲染器渲染输出
今天的话题就发生在请求格式为html的HtmlRenderer渲染器中,这个渲染器用到了标题解析器
这就是本主题的内容,下面看看标题是怎么计算出来的:

首先如果控制器返回的渲染数组包含#title子元素,如:$main_content['#title'],那么将原封不动的使用其值,标题计算完成
如果没有包含,则调用标题解析器进行计算,标题解析器服务id为“title_resolver”
类:Drupal\Core\Controller\TitleResolver 构造函数接收控制器解析器:controller_resolver及字符串翻译服务:string_translation

下面看一下标题解析源代码:

 

public function getTitle(Request $request, Route $route) {
    $route_title = NULL;
    // A dynamic title takes priority. Route::getDefault() returns NULL if the
    // named default is not set.  By testing the value directly, we also avoid
    // trying to use empty values.
    if ($callback = $route->getDefault('_title_callback')) {
      $callable = $this->controllerResolver->getControllerFromDefinition($callback);
      $arguments = $this->controllerResolver->getArguments($request, $callable);
      $route_title = call_user_func_array($callable, $arguments);
    }
    elseif ($title = $route->getDefault('_title')) {
      $options = array();
      if ($context = $route->getDefault('_title_context')) {
        $options['context'] = $context;
      }
      $args = array();
      if (($raw_parameters = $request->attributes->get('_raw_variables'))) {
        foreach ($raw_parameters->all() as $key => $value) {
          $args['@' . $key] = $value;  //@和%表示要进行转义的状态,见下文解释
          $args['%' . $key] = $value;
        }
      }
      if ($title_arguments = $route->getDefault('_title_arguments')) {
        $args = array_merge($args, (array) $title_arguments);
      }

      // Fall back to a static string from the route.
      $route_title = $this->t($title, $args, $options);
    }
    return $route_title;
  }

 

首先查找路由定义中是否设置有标题回调,设置格式和控制器完全一样,键名为_title_callback,对回调的执行和控制器完全一样
通过标题回调得到的结果不会被翻译,需要翻译需要在回调中进行
如果没有设置回调则查找_title子元素,如果设置了则进行翻译工作(翻译系统介绍请看后续主题
检查是否设置翻译上下文_title_context 此上下文用于翻译过程的消除歧义,比如may 到底翻译成“可以”还是“五月”呢?一般为空
准备翻译参数,将请求中的_raw_variables值放入参数数组,_raw_variables仅包含占位符变量的原始值,不包括路由额外提供的变量
然后检查路由是否提供额外标题参数,如果有则合并入参数数组

然后将标题、参数、上下文传递给翻译系统进行翻译,标题计算工作完成

优先级总结:控制器指定 》标题回调 》路由标题指定

关于翻译系统的参数解释见:https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Render%21FormattableMarkup.php/function/FormattableMarkup%3A%3AplaceholderFormat/8.2.x

翻译系统上下文解释见:https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Language%21language.api.php/group/i18n/8.2.x

 

下面给出一个完整冗余的标题路由定义:

 

yunke.content:
  path: '/yunkehello'
  defaults:
    _controller: '\Drupal\yunke\Controller\HelloController::content'
    _title_callback: '\Drupal\yunke\Controller\HelloController::getTitle'
    _title: 'Hello @user ! time:@May'
    _title_context:'Long month name'
    _title_arguments: 
      @May: "May"
      @user: "yunke" 
  requirements:
    _permission: 'access content'

 

 

我是云客,【云游天下,做客四方】欢迎转载,但须注明出处,讨论请加qq群203286137

 

 

Drupal 版本: 

云客Drupal8源码分析之渲染数组(render array)

从本质上讲现代所有的web软件系统中都用到了渲染数组,只不过在drupal世界里明确给了它这个名字:“渲染数组”。

如果你使用过模板引擎,那么会很熟悉它,要使模板引擎工作,那么需要给它传入一些变量,这些变量将决定模板里面对应变量的显示,传入的这些变量往往是以数组的方式传入,键名代表模板里面的变量名,键值代表变量值,这样的数组就叫做渲染数组,比如著名的php小型框架CodeIgniter(简称CI框架)中的经典用法就是这样:

 

$this->load->view('show_news', $data);

其中参数show_news指定网页模板,参数$data就是程序中准备好的一个数组,包含了传递给模板的变量

 

可以说渲染数组是数据系统和显示系统的一个桥梁,在数据系统中程序计算出需要给用户的数据,他们全部放置到渲染数组中,显示系统仅面向渲染数组这一个接口工作,这样一来系统和显示之间解耦了,许多事情变得简单,这里的显示是广义的,它也包括“显示”成json或xml给其他系统“看”。

由此可见渲染数组的重要性,相比CI框架等轻量级系统,drupal中的渲染数组则更上一层楼,它包含更多的功能,在drupal中渲染数组不仅包含传递给模板的变量,还包含缓存数据,附件数据,在不用模板的情况下如何进行渲染的数据,下面来看一看drupal的渲染数组:

在drupal中控制器只有三种行为:返回响应对象、返回渲染数组、抛出异常。如果返回的是渲染数组,将派发视图事件,此事件将触发渲染系统,渲染系统启动渲染器对渲染数组进行渲染,在这个过程中渲染器根据渲染数组包含的信息视情况启动主题子系统,也可能直接进行渲染,最后输出内容。渲染数组在整个系统处理流程中大多被以引用传递给各种处理函数或方法,以便于模块等对渲染数组进行修改。

先给出一个简单的列子,看看渲染数组长什么样:

 

$page = [
  '#type' => 'page',
  'content' => [
    'system_main' => […],
    'another_block' => […],
    '#sorted' => TRUE,
  ],
  'sidebar_first' => [
    …
  ],
];

在drupal中“渲染”是什么意思?就是将一个结构化的数组(render array)转换成某格式的字符串,比如html字符串(页面),这个结构化的数组被称为渲染数组。
渲染数组是一个有许多层级的关联嵌套数组,其子元素分为两大类:以#开头的键名表示属性,用于指示怎么渲染及缓存等等,没有以#开头的任意其他命名的元素表示children(理解为子渲染元素、下一层级元素、子内容,html元素是嵌套的),在渲染数组中children子元素必是一个数组类型,他代表下一层级的渲染数组。
属性名是特定的,子内容名是灵活的,它可以是代表分区的变量名(分区概念见后续主题)
在脑海中可以这样理解:渲染数组就像DOM在drupal世界中的体现,从上层元素到下层元素逐级嵌套,形成一棵渲染树

 

 

 

看看常用的渲染数组属性,根据它的作用体会渲染数组怎么工作:

 

#type 可选,但通常均有设置,指定元素类型,并非是指html元素类型,是更高一级的抽象,设置它的目的是为了向渲染数组附加该类型默认的渲染属性,详见本系列的渲染类型主题,不指定或指定的元素类型不存在将不附加任何值,见下文的元素类型。
#theme 可选,定义一个主题钩子回调,这个回调将调用主题模块对渲染数组进行解析转变为html字符串,通常是使用模板引擎,传递给模板的变量可以包含在渲染数组中并用#开头,比如模板中变量名为yunke,那么渲染数组键名为#yunke
#markup 可选,渲染数组直接提供的html标签字符串,可以是嵌套的html片段,值表现为字符串类型或Markup对象,一旦被标记为Markup对象则认为它是安全的(经过转义的),一般使用#type或#theme代替这个选项,这样可以让主题自定义标签
#plain_text 可选,是一个未经过转义的html字符串文本,如果设置,它将被转义处理,并替换#markup的值
#allowed_tags 可选,如果#markup的值不是Markup对象而是一个字符串,则需要进行转义并转换成Markup对象,此项表示此过程中允许使用的标签,如果不设置默认使用Drupal\Component\Utility\Xss::getAdminTagList()返回的标签,此项设置对#plain_text没有影响
#access_callback  可选,访问检查回调,和控制器定义方法相同
#access  可选,如果设置此项则忽略#access_callback项,访问检查,值可以是AccessResultInterface类型,也可以是布尔类型,如果不被允许则不会渲染数组,返回空字符串
#printed  可选,布尔值,标记是否已经被处理成了html字符串内容,渲染时使用empty判断此属性,如果为非空值这不会进行渲染,返回空字符串
#defaults_loaded 可选,指示是否已经向本渲染数组附加合并了对应元素类型的默认渲染属性,此值为空则加载#type所指元素类型的默认渲染属性
#lazy_builder 可选,数组值,必须有且只有两个元素,一个回调,一个回调的参数,参数只能是NULL或者标量类型,当指定这个选项时不能够指定children元素及'#cache','#create_placeholder','#weight','#printed'之外的属性,它们应该在回调中产生
#lazy_builder_built 可选,布尔值,内部使用,指示元素由#lazy_builder回调产生
#create_placeholder 可选,布尔值,是否自动创建占位符,当指定此选项时,#lazy_builder必须存在
#pre_render 可选,值是一个回调构成的数组,这些回调函数能在渲染前修改渲染数组,比如重新排序、修改、删除某部分等,回调可以设置添加#printed,如果调用后数组中设置了#printed,其值不为空将终止渲染
#post_render 可选,一个由回调构成的数组,在渲染子元素并追加到#markup之后,依次调用这些回调,它们可以修改之前的渲染输出,某些方面有点类似于#theme_wrappers,只是不使用主题子系统
#states 可选,代表元素在浏览器中的状态信息,选中、隐藏等等,详细见函数:core\includes\common.inc : drupal_process_states
#sorted 可选,布尔值,代表子元素是否已经被排序过
#weight 可选,代表子元素在父元素中排序的权重,支持三位小数精度
#attributes 可选,代表元素的html属性,其值为一个数组,此数组的键名为属性名,键值为渲染数组对应于html属性名的属性元素的值,详见\core\lib\Drupal\Core\Render\Element.php中的setAttributes方法
#children 内部使用,表示被渲染后的子元素,字符串类型或者Markup对象
#theme_wrappers 一个由主题钩子回调组成的数组,当子元素被渲染赋值给#children后,依次调用它们,它们使用主题子系统,常用于给子元素的html内容加上外层包装,这个选项用的比较少,通常使用#theme
#render_children 可选,程序内部使用,如果设置,则不会使用#theme_wrappers和#theme中的主题钩子函数,防止无限递归
#prefix 可选,html内容字符串,经过安全处理后加到子元素前面
#suffix 可选,html内容字符串,经过安全处理后加到子元素后面
#title 可选,html页面标题,如果未设定,将使用标题解析器从路由设置等地方计算出标题,
#cache_properties 可选,在进行缓存的时候将被保留的属性或子内容,默认值只缓存渲染数组中的#markup、#attached、#cache属性,如果保留子内容也仅是缓存子内容的#markup属性

由于每种元素类型都有自己的属性,因此在渲染数组中根据不同元素类型就有不同的以#开头的键名(属性),此外传递给模板的变量也是以#加变量名储存在渲染数组中,所以渲染数组的属性数量庞大,在这里仅例举了最常见的属性
更多属性见:https://api.drupal.org/api/drupal/developer%21topics%21forms_api_reference.html/7.x
渲染数组被Form API 和 Render API共用,但数组中有些表单元素的属性只对于表单API有意义,将这样的渲染数组用到渲染API时需要通过FormBuilder转换,否则可能有不能预料的结果。

特别说明:
#attached 可选,代表渲染元素的附件,渲染数组各个层级的附件会被渲染上下文对象跟踪收集,有效子元素有:
   'library':css或js库,统称资源库
   'drupalSettings':前端js设置 (JavaScript settings)
   'feed':内容聚合RSS feeds
   'html_head':在HTML <head>元素里面的标签
   'html_head_link':在HTML <head>里面的<link>标签
   'http_header':HTTP响应头和状态码
   'placeholders':见下文的占位符介绍。注意:替换值本身也可以是渲染数组,此时它被渲染后再去替换占位符,缓存标签照样冒泡影响被替换的元素

#cache 可选,指定渲染数组的缓存参数,drupal能够通过此设置分别缓存渲染数组各个层级的渲染输出,这样减少计算量,提高性能,各层级的缓存数据会向上冒泡以决定上一层级的缓存性质,有效子键名为:
   'keys':用于指定缓存id,在内部产生这个id还会结合contexts的值,渲染数组要缓存时才设置这个项
   'contexts':指定缓存上下文
   'tags':指定缓存标签
   'max-age':指定缓存最大时长,秒为单位,是最大时长,不是到期时间,永不过期为-1,默认为永久
   'bin':指定缓存储存位置,如不指定默认为render,默认放在数据库的render缓存表中

 

 

 

在渲染数组中#cache和#attached特别重要,渲染过程中他们会从渲染树中逐级冒泡,直到文档根元素,这样才能保证所有元素的缓存正确,以及收集所有需要用到的js、css,为此系统建立了一个叫做渲染上下文的对象来跟踪他们, 渲染上下文会一直跟踪渲染递归过程,收集整个页面需要的所有附件,并判断渲染数组各个层级的可缓存性

 

为什么渲染数组需要渲染占位符:
1:有时候页面是一模一样的,仅仅某一元素不一样,比如一个页面仅显示的用户名不一样,如果有一万的用户岂不是要缓存一万份?这是不划算的,也不合理
2:有些内容变化频率特别快,几秒钟就变化,甚至更短,这个时候更新缓存的成本相对就大,使用缓存不划算
3:有些内容很快就过期,缓存没什么价值,还会浪费缓存执行成本
综上所述三条,你会发现他们就对应缓存三要素:上下文、标签、时间,由于这些原因导致缓存不划算花费过大,所以设置占位符机制,将这些缓存不划算的内容和划算的内容隔离开,仅缓存划算的内容,从缓存取出数据后再根据占位符信息将缓存不划算的内容实时替换进去,可以看出渲染数组的占位符机制是非常有用的。
判断页面某部分缓存不划算而运用占位符的过程叫做“Auto-placeholdering”(自动占位过程),它是怎么自动的呢?就得依据渲染配置,容器参数中的renderer.config就是指定此配置默认值如下:

 

[renderer.config] => Array
                (
                    [required_cache_contexts] => Array
                        (
                            [0] => languages:language_interface
                            [1] => theme
                            [2] => user.permissions
                        )

                    [auto_placeholder_conditions] => Array
                        (
                            [max-age] => 0
                            [contexts] => Array
                                (
                                    [0] => session
                                    [1] => user
                                )

                            [tags] => Array
                                (
                                )
                        )

可以看到基本没什么问题的设置均被指定为默认配置了,更多的内容就要依据站点的实际情况,这也是站点优化的一个主要内容。
有#lazy_builder的元素会被运用自动占位机制,因为#lazy_builder代表延迟构建,往往不是靠渲染数组来构建页面,而是使用一个回调
注意占位符会在渲染最后一刻才执行替换,这个机制允许系统缓存许多碎片数据。

元素类型:
在drupal中元素类型 "element types"本质上是一个预包装好的默认渲染数组,渲染数组指定了元素类型将附加这些默认渲染属性,模块可以定义元素类型,创建一个实现了ElementInterface的RenderElement插件即可,关于此块内容请看本系列的渲染元素类型主题

参考链接:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/group/theme_render/8.2.x
https://www.drupal.org/docs/8/api/render-api

 

 

 

drupal的渲染系统内容很多,需要分多个主题进行介绍,这里先介绍最最重要的渲染数组,其他内容请看后续主题
有疑问没有关系,等学习完后续主题后便能云开雾散

 

我是云客,【云游天下,做客四方】欢迎转载,但须注明出处,讨论请加qq群203286137

 

 

Drupal 版本: 

云客Drupal8源码分析之渲染上下文RenderContext、渲染器renderer

当drupal的控制器返回渲染数组的时候,系统会派发视图事件,渲染数组被main_content_view_subscriber(主内容视图订阅器)处理,它根据请求的格式,将系统流程定向到对应的格式渲染器,系统默认提供了四个格式渲染器,他们被定义在容器的main_content_renderers参数里面,其中html格式对应的是服务id为“main_content_renderer.html”的html格式渲染器,我们得到的html格式页面几乎都是它渲染的,它将渲染分为两个步骤:先渲染body标签,然后渲染html标签,其中我们将html的渲染称为根渲染。

具体的工作是由渲染器完成的,它负责将渲染数组转换为html字符串,本篇的主题就是这个渲染器。

要理解它是怎么工作的需要先理解一些知识。

首先是渲染数组,关于它可阅读本系列的《云客Drupal8源码分析之渲染数组(render array)》

其次是渲染上下文,下面介绍一下渲染上下文:

渲染上下文:

每次渲染时,渲染数组的渲染过程是从根元素开始的,递归到所有的子元素,它对应着html页面结构,由父元素渲染到子元素,这个过程中有两个问题需要解决:

1:缓存问题,为了提高性能,每个渲染数组只渲染一次,除非变的无效,否则不会再次渲染,这里子元素的缓存性质将影响到父元素,比如子元素一小时后失效,那么父元素的缓存时间也不能超过一小时

2:附件问题,每个元素都可以定义自己要用到的js或css等附件,需要收集所有的这些附件,以添加到最终的页面上,这个收集过程就伴随渲染过程实现的

为了解决以上两个问题,系统定义了一个BubbleableMetadata对象(Drupal\Core\Render\BubbleableMetadata),字面翻译为“可冒泡元数据”,每个BubbleableMetadata对象都对应一个元素(或称为渲染数组,可含有子元素),它包含着该元素的缓存数据和附件数据,但根据它的用途,云客这里将它称为“渲染跟踪元数据”,为什么取了BubbleableMetadata这么个名字呢?为什么是冒泡?这跟渲染数组的遍历算法有关系,如果学过计算机数据结构这门课程会比较熟悉冒泡遍历,没学过?没关系后面会讲清楚。

对渲染数组的渲染(遍历)是从根元素开始的,它可能有很多层子元素,每一层也可能有多个并列子元素,渲染并不是将父层渲染完后再开始渲染子层(这叫做广度优先算法),而是使用深度优先算法(将一个元素和它的所有子元素渲染完才会开始渲染兄弟元素),如果把“渲染”比作一个人,它将在元素间不停进出或平移,他的行动轨迹完全符合堆栈数据结构,实际上系统就设置了一个堆栈对象来表示这个轨迹,每个元素对应着一个BubbleableMetadata,这个人在进行渲染工作的时候,只需要将元素对应的BubbleableMetadata对象在堆栈对象中压入、弹出、合并就能解决上面两个问题,这个堆栈对象就是本主题说的渲染上下文对象:Drupal\Core\Render\RenderContext,当渲染进入一个子元素的时候压入一个空的BubbleableMetadata对象到堆栈中,渲染完后更新这个对象,当退出这个元素的时候将BubbleableMetadata和堆栈中前一个BubbleableMetadata合并,这个合并动作就是冒泡,它将子元素的缓存数据和附件数据带到父元素,现在明白为什么叫做冒泡了吧,渲染过程中缓存数据逐级向上传递,附件也全部收集到一起。

这里需要注意的是:“渲染”这个“人”在渲染完某个元素,更新当前的BubbleableMetadata时,也可能依据BubbleableMetadata将渲染结果进行缓存,这样每个元素只渲染一次,每个满足缓存条件的子元素都被独立缓存了,这样大大提高了系统性能。

 

介绍完上面的准备知识后我们来看一下渲染器是怎么工作的:

渲染器:

渲染器的服务id是:renderer
类:Drupal\Core\Render\Renderer
实现了渲染器接口:Drupal\Core\Render\RendererInterface
有以下构造参数,以服务id指出:
controller_resolver:解析渲染数组中的回调,见控制器解析器
theme.manager:主题系统,进行主题渲染
plugin.manager.element_info:用于获取元素的默认值
render_placeholder_generator:产生占位符,占位符请看渲染数组主题
render_cache:执行渲染缓存
request_stack:请求堆栈,用于获取当前请求的渲染上下文,它以请求对象为数组键,保存在属性数组里
以及必须的渲染上下文及占位符条件配置数组

drupal的渲染系统是一个很大的系统,各种对象配合工作,由于篇幅原因本主题不介绍以上对象的内容,但不用担心,他们不影响本主题的理解,他们将在后续主题中介绍,现在只需要知道他们的作用足矣。

渲染器的核心方法是doRender,在这个方法中能明白渲染的主要工作,下面是对这个方法的流程说明,请对照着程序看:

见Drupal\Core\Render\Renderer::doRender

由于要自我调用渲染子数组,所以其第二个参数$is_root_call表明当前调用是否处于自我递归调用中,仅内部使用,并不是指在渲染根html元素

这里以变量:$elements做为渲染数组来说明:

如果$elements为空直接返回空串

先进行权限检查
如果没有设置$elements['#access']时,看有无访问控制回调$elements['#access_callback'],如果有执行回调,将结果赋值给$elements['#access']
如果$elements['#access']全等为false,则返回空串,如果为AccessResultInterface对象,将其缓存依赖添加到渲染数组,以便权限正确,访问结果不允许的话也返回空串

如果$elements['#printed']不为空,说明已经被渲染过了,直接返回空(笔者认为返回$elements['#markup']更好)
准备开始渲染实质性工作
取得上下文堆栈对象,它用于跟踪各级渲染数组的缓存数据和附件数据,压入一个初始的BubbleableMetadata对象,字面翻译是“可冒泡元数据”,按用途和直观原因笔者这里翻译为:“渲染跟踪元数据”,是如何跟踪的呢?伴随渲染递归过程,从最里层子元素冒泡到根父元素

如果有$elements['#cache']['keys']属性或者是根渲染,则合并预配置的必要缓存上下文。

如果设置有$elements['#cache']['keys']则尝试从渲染缓存里面获取渲染结果,渲染缓存里面的内容都是没有经过占位符替换的,所以如果是递归子元素完毕后的根渲染,则返回前要进行占位符替换

如果存在$elements['#type']则附加元素的默认值,其中如果设置了$elements['#defaults_loaded']不为空将阻止加载,这在某些方面是有用的,比如表单API里面的form_builder()

判断是否设置延迟构建回调$elements['#lazy_builder'],并检查它的合法性;它的作用就是延迟构建渲染数组,其值只能是两个元素组成的数组:回调及回调的参数,参数可以是NULL或者标量类型值;当指定延迟构建时,渲染数组不能也不应该有子渲染数组,子内容应该由延迟构建回调产生

判断是否需要运用自动占位符机制,如果需要,则设置$elements['#create_placeholder']为真,当$elements['#create_placeholder']有设置且为真时$elements['#lazy_builder']必须存在,因为需要用它的结果去替换占位符
当$elements['#create_placeholder']有设置且为真时对$elements创建占位符

如果存在$elements['#lazy_builder']则调用它来构建新元素以代替原元素,将原元素的缓存数据设置到新元素,设置$elements['#lazy_builder_built']为TRUE以表示已经调用了延迟构建

如果设置了$elements['#pre_render']则调用它们,允许其修改渲染数组

如果$elements['#markup']或$elements['#plain_text']不为空,则处理它们,$elements['#plain_text']优先级更高,保证有一个安全的$elements['#markup'],其值为markup对象

规范化当前元素的渲染跟踪元数据

检查$elements['#printed']的设置,允许$elements['#pre_render']终止渲染

如果存在$elements['#states']则处理状态信息

查找出子元素(子渲染数组),如果$elements['#children']不存在则设置它的初始值为空字符串,它代表子元素的html内容字符串

如果设置了$elements['#theme'],则说明渲染数组需要由主题系统去渲染,调用主题系统进行渲染,这也将渲染子元素,并将结果赋值给$elements['#children'],可以设置$elements['#render_children']防止调用主题渲染

当没有被主题系统渲染或设置了$elements['#render_children']防止主题渲染,此时当$elements['#children']为空,则进行子渲染,自我调用,这里是递归渲染的开端

如果存在$elements['#markup'],将它追加到$elements['#children']前面,如果是主题渲染的话,此工作应该由主题系统完成

如果存在$elements['#theme_wrappers']且没有设置$elements['#render_children'],则由主题系统执行主题包装渲染,它通常在已经渲染的子元素外面包装一些其他元素

如果存在$elements['#post_render']则执行这些回调

如果有设置前缀$elements['#prefix']或后缀$elements['#suffix']则运用它们,最终形成渲染后的$elements['#markup'],它是一个Markup对象

执行渲染上下文堆栈的更新,具体是将代表本元素的BubbleableMetadata(渲染跟踪元数据)变为最新,因为上面许多过程可以改变这些值,比如#pre_render、#post_render、#lazy_builder等等

缓存已经被渲染的渲染数组,在以上整个过程中$elements['#cache']['keys']不允许被改变,需要不同的缓存id可以使用缓存上下文去实现

如果是递归渲染结束后的根渲染,则进行占位符替换,将真正的占位内容值渲染后替换占位变量

执行冒泡:$context->bubble(),这是渲染跟踪元数据冒泡的关键,也是渲染上下文堆栈对象发挥作用的核心

最后标记$elements['#printed']为真表示已经渲染过了,返回$elements['#markup'],它代表渲染后的字符串值,以markup对象的方式存在

以下做一些补充说明:

其实drupal8的所有控制器都是包装在一个渲染上下文中执行的,目的是解决在控制器中直接渲染内容丢失BubbleableMetadata的问题,有望在drupal9中通过禁止在控制器中直接渲染解决

渲染器进行根渲染后js和css附件仅通过占位tokens代替,在后续流程中才会真正附加这些附件

doRender等方法的$is_root_call参数代表当前调用是否为一个递归调用,并不表示在进行html文档的根渲染,它是一个编程技巧,仅内部使用,在递归过程中不会进行占位符替换

渲染器的$contextCollection属性,它是SplObjectStorage对象,保存请求堆栈中每个请求对应的请求上下文堆栈对象,注意它是静态属性,这意味着不管多少个渲染器实例,渲染上下文保证每个请求只对应一个,在executeInRenderContext方法中会临时设置一个上下文,用完即毁,然后复原之前的上下文。

强调一下在渲染过程中$elements['#cache']['keys']不允许被改变

 

我是云客,【云游天下,做客四方】微信号:php-world 欢迎转载,但须注明出处,讨论请加qq群203286137

 

 

 

 

 

 

 

 

Drupal 版本: