├── SUMMARY.md ├── LICENSE ├── README.md ├── 11.md ├── 06.md ├── 04.md ├── 02.md ├── 12.md ├── 01.md ├── 03.md ├── 08.md ├── 10.md ├── 13.md ├── 09.md ├── 05.md └── 07.md /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | 这个栏目暂时告一段落,前一部分比较像Yii,后一部分比较像Laravel,因为当时正在看相应框架的源码,所以会有不少借鉴参考。捂脸~ 4 | 5 | 这个框架千万不要直接应用于生产环境,只是用来帮助大家理解PHP框架的实现机制。 6 | 7 | * [创建自己的PHP框架](README.md) 8 | * [搭建基本结构](01.md) 9 | * [抽象框架的内容](02.md) 10 | * [抽象Controller的基类](03.md) 11 | * [定义ORM的接口](04.md) 12 | * [实现Model类(1)](05.md) 13 | * [实现Model类(2)](06.md) 14 | * [实现Model类(3)](07.md) 15 | * [创建组件的机制](08.md) 16 | * [构建缓存组件(1)](09.md) 17 | * [构建缓存组件(2)](10.md) 18 | * [构建模版引擎(1)](11.md) 19 | * [构建模版引擎(2)](12.md) 20 | * [构建模版引擎(3)](13.md) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Harry Sun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-your-own-php-framework 2 | 构建自己的PHP框架 3 | 4 | 这个栏目暂时告一段落,前一部分比较像Yii,后一部分比较像Laravel,因为当时正在看相应框架的源码,所以会有不少借鉴参考。捂脸~ 5 | 6 | 这个框架千万不要直接应用于生产环境,只是用来帮助大家理解PHP框架的实现机制。 7 | 8 | * [搭建基本结构](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/01.md) 9 | * [抽象框架的内容](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/02.md) 10 | * [抽象Controller的基类](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/03.md) 11 | * [定义ORM的接口](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/04.md) 12 | * [实现Model类(1)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/05.md) 13 | * [实现Model类(2)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/06.md) 14 | * [实现Model类(3)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/07.md) 15 | * [创建组件的机制](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/08.md) 16 | * [构建缓存组件(1)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/09.md) 17 | * [构建缓存组件(2)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/10.md) 18 | * [构建模版引擎(1)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/11.md) 19 | * [构建模版引擎(2)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/12.md) 20 | * [构建模版引擎(3)](https://github.com/CraryPrimitiveMan/create-your-own-php-framework/blob/master/13.md) 21 | -------------------------------------------------------------------------------- /11.md: -------------------------------------------------------------------------------- 1 | 前段时间太忙,导致好久都没有更新博客了,今天抽出点时间来写一篇。 2 | 3 | 其实这个系列的博客很久没有更新了,之前想好好规划一下,再继续写,然后就放下了,今天再捡起来继续更新。 4 | 5 | 今天我们来说一下,如何构建自己的 PHP 模版引擎。现在比较流行的 PHP 模版引擎有[Twig](http://twig.sensiolabs.org/)、[Haml](https://github.com/arnaud-lb/MtHaml)、[Liquid](https://github.com/harrydeluxe/php-liquid)、[Mustache](http://mustache.github.io/)、[Plates](http://platesphp.com/)、[Blade](https://laravel.com/docs/5.3/blade)以及比较古老的[Smarty](http://www.smarty.net/) 6 | 7 | 其实关于PHP应不应该使用模版引擎,网上也有不少争论,在这里罗列一些使用模版引擎的优点。 8 | 9 | + 安全,比如默认转义输出 10 | + 可读性好 11 | 12 | 相关内容可以参考一下知乎的讨论 13 | 14 | + [PHP 模板引擎有多大意义?](https://www.zhihu.com/question/19674848) 15 | + [为什么PHP中ThinkPHP有做类似模板引擎的东西?smarty也是?这些到底有何用?](https://www.zhihu.com/question/26053623) 16 | 17 | 首先末来确定一下思路,我们先要确定在模版中使用怎样的写法,参考 Laravel 的 Blade 模板,定义如下几种简单的写法。 18 | 19 | + 输出变量值 20 | 21 | `{{ }}` 表达式的返回值将被自动传递给 PHP 的 htmlentities 函数进行处理,以防止 XSS 攻击。 22 | 23 | ```php 24 | Hello, {{ $name }}! 25 | ``` 26 | 27 | + 输出未转义的变量值 28 | 29 | 30 | ```php 31 | Hello, {!! $name !!}! 32 | ``` 33 | 34 | + If 表达式 35 | 36 | 通过 @if、@elseif、@else 和 @endif 指令可以创建 if 表达式。 37 | 38 | ```php 39 | @if (count($records) === 1) 40 | I have one record! 41 | @elseif (count($records) > 1) 42 | I have multiple records! 43 | @else 44 | I don't have any records! 45 | @endif 46 | ``` 47 | + 循环 48 | 49 | ```php 50 | @for ($i = 0; $i < 10; $i++) 51 | The current value is {{ $i }} 52 | @endfor 53 | 54 | @foreach ($users as $user) 55 |
This is user {{ $user->id }}
56 | @endforeach 57 | 58 | @while (true) 59 |I'm looping forever.
60 | @endwhile 61 | ``` 62 | 63 | + 引入其他视图 64 | 65 | ```php 66 | @include('view.name', ['some' => 'data']) 67 | ``` 68 | 69 | 暂时先定义这么多,基本够用。如果有特殊的需要,可以自己添加,其原理基本是一致的,会写一个,其他的就都能写出来。 70 | 71 | 然后再考虑如何处理,我们定义出了这样的写法,PHP 是识别不了的,我们需要将它转化成 PHP 能够识别的样子。 72 | 73 | 举个最简单的例子,当我们拿到`{{ $name }}`这样一段内容时,我们只需要将它转化成``这样,就可以识别了,输出相应的变量值。 74 | 75 | 是不是很简单,好像只需要用正则替换一下就可以了,具体实现我在下一篇文章中继续说~ 76 | 77 | 然后我们还要考虑到性能,每次我们都要做这样的转化,是不是很耗性能,我们可以每次都将转化后的结果缓存起来,当文件发生变化后,再重新转化,这样是不是就解决了一部分性能的问题~~ 78 | 79 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 80 | 81 | code:https://github.com/CraryPrimitiveMan/simple-framework/tree/1.0 82 | 83 | blog project:https://github.com/CraryPrimitiveMan/create-your-own-php-framework 84 | -------------------------------------------------------------------------------- /06.md: -------------------------------------------------------------------------------- 1 | 在上一篇博客中我们简单实现了findOne方法,但我们可以看到,还是有一些问题的,下面我们来修正一下这些问题。 2 | 3 | 首先是返回的数据中,数字被转换成了字符串。我们需要的是数字啊。。。 4 | 5 | PDO中有属性可以支持,PDO::ATTR_STRINGIFY_FETCHES 和 PDO::ATTR_EMULATE_PREPARES 属性。 6 | 7 | + PDO::ATTR_STRINGIFY_FETCHES: 提取的时候将数值转换为字符串。 需要 bool 类型。 8 | + PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。 有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为 TRUE ),或试着使用本地预处理语句(如果为 FALSE)。如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上。 需要 bool 类型。 9 | 10 | 将这两个参数设置为false,应该就可以使返回的数据中的数字保持数字格式了。 11 | 12 | 相关详情可查看[PDO预定义常量](http://php.net/manual/zh/pdo.constants.php)和[PDO::setAttribute](http://php.net/manual/zh/pdo.setattribute.php) 13 | 14 | getDb方法修改为如下内容: 15 | 16 | 17 | ```php 18 | public static function getDb() 19 | { 20 | if (empty(static::$pdo)) { 21 | $host = 'localhost'; 22 | $database = 'sf'; 23 | $username = 'jun'; 24 | $password = 'jun'; 25 | $options = [ 26 | PDO::ATTR_EMULATE_PREPARES => false, 27 | PDO::ATTR_STRINGIFY_FETCHES => false 28 | ]; 29 | static::$pdo = new PDO("mysql:host=$host;dbname=$database", $username, $password, $options); 30 | static::$pdo->exec("set names 'utf8'"); 31 | } 32 | 33 | return static::$pdo; 34 | } 35 | ``` 36 | 37 | 但是修改完,之后你可能发现,根本不起作用,那究竟是什么原因呢?其实是你用的是老的驱动php5-mysql,你应该换成php5-mysqlnd。执行如下代码更换驱动(Ubuntu环境下): 38 | 39 | ```shell 40 | # Remove the old driver 41 | sudo apt-get remove php5-mysql 42 | # Install the new driver 43 | sudo apt-get install php5-mysqlnd 44 | ``` 45 | 46 | [关于这个问题的详情可点击此处查看](http://stackoverflow.com/questions/20079320/php-pdo-mysql-how-do-i-return-integer-and-numeric-columns-from-mysql-as-int) 47 | 48 | 然后之前的代码里还有一个问题,当findOne的参数为空时,会挂掉。所以需要对$condition做一下判空。修正完之后的代码如下: 49 | 50 | ```php 51 | public static function findOne($condition = null) 52 | { 53 | $sql = 'select * from ' . static::tableName(); 54 | $params = []; 55 | 56 | // 判空 57 | if (!empty($condition)) { 58 | $sql .= ' where '; 59 | $params = array_values($condition); 60 | $keys = []; 61 | foreach ($condition as $key => $value) { 62 | array_push($keys, "$key = ?"); 63 | } 64 | $sql .= implode(' and ', $keys); 65 | } 66 | 67 | $stmt = static::getDb()->prepare($sql); 68 | $rs = $stmt->execute($params); 69 | 70 | if ($rs) { 71 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 72 | if (!empty($row)) { 73 | $model = new static(); 74 | foreach ($row as $rowKey => $rowValue) { 75 | $model->$rowKey = $rowValue; 76 | } 77 | return $model; 78 | } 79 | } 80 | 81 | return null; 82 | } 83 | ``` 84 | 85 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 86 | 87 | code:[https://github.com/CraryPrimitiveMan/simple-framework/tree/0.6](https://github.com/CraryPrimitiveMan/simple-framework/tree/0.6) 88 | 89 | blog project:[https://github.com/CraryPrimitiveMan/create-your-own-php-framework](https://github.com/CraryPrimitiveMan/create-your-own-php-framework) -------------------------------------------------------------------------------- /04.md: -------------------------------------------------------------------------------- 1 | # 构建自己的PHP框架--定义ORM的接口 2 | 3 | 在上一篇博客中,我们抽象出了Controller的基类,实现了页面的渲染和返回JSON字符串的功能。 4 | 5 | 那作为一个框架,我们现在还缺少什么?是的,大家应该已经注意到了,我们在这之前从来没有连接过数据库,我们缺少一个ORM(Object Relational Mapping)。 6 | 7 | 在php中连接mysql有三种方式,分别是使用原生函数、mysqli扩展和PDO扩展,详细内容可以查看我之前的博客《[PHP的学习--连接MySQL的三种方式](http://www.cnblogs.com/CraryPrimitiveMan/p/4385034.html)》。 8 | 9 | 我们要选择哪一种呢?考虑到作为一个框架不能仅支持一种数据库,我们就选择使用PDO。当然如果你确定你的框架只需要连接mysql数据库,也可以考虑使用mysqli。 10 | 11 | PDO支持如下的数据库: 12 | 13 | * [CUBRID (PDO)](http://php.net/manual/zh/ref.pdo-cubrid.php) 14 | * [MS SQL Server (PDO)](http://php.net/manual/zh/ref.pdo-dblib.php) 15 | * [Firebird (PDO)](http://php.net/manual/zh/ref.pdo-firebird.php) 16 | * [IBM (PDO)](http://php.net/manual/zh/ref.pdo-ibm.php) 17 | * [Informix (PDO)](http://php.net/manual/zh/ref.pdo-informix.php) 18 | * [MySQL (PDO)](http://php.net/manual/zh/ref.pdo-mysql.php) 19 | * [MS SQL Server (PDO)](http://php.net/manual/zh/ref.pdo-sqlsrv.php) 20 | * [Oracle (PDO)](http://php.net/manual/zh/ref.pdo-oci.php) 21 | * [ODBC and DB2 (PDO)](http://php.net/manual/zh/ref.pdo-odbc.php) 22 | * [PostgreSQL (PDO)](http://php.net/manual/zh/ref.pdo-pgsql.php) 23 | * [SQLite (PDO)](http://php.net/manual/zh/ref.pdo-sqlite.php) 24 | * [4D (PDO)](http://php.net/manual/zh/ref.pdo-4d.php) 25 | 26 | 当然,这些数据库即使都可以使用PDO去连接使用,但在某些具体的情况下,还是有些许不同的,详情可参考[PDO文档](http://php.net/manual/zh/book.pdo.php) 27 | 28 | 鉴于我电脑现在只安装了mysql,之后的code,只会测试mysql数据库,不会测试其他数据库。 29 | 30 | 首先我们会将这些内容放在src/db文件夹中,我们需要定义一下接口,这里我们会先安最简单的来。 31 | 32 | 我们需要实现什么?最简单的就是数据的增删改查。 33 | 34 | 假设我们现在有一张article表,一个与之对应的Model Article,我们希望怎么用它呢? 35 | 36 | ```php 37 | // 选出id为1的一篇文章 38 | $article = Article::findOne(['id' => 1]); 39 | 40 | // 选出status是unpublished的所有文章 41 | $articles = Article::findAll(['status' => 'unpublished']); 42 | 43 | // 将id为1的所有文章的status更新为published 44 | Article::updateAll(['id' => 2], ['status' => 'published']); 45 | 46 | // 删除所有id为1的文章 47 | Article::deleteAll(['id' => 2]); 48 | 49 | // $article是之前选出的id为1的文章 50 | // 更新它的属性status为unpublished 51 | $article->status = 'unpublished'; 52 | // 保存更新到数据库 53 | $article->update(); 54 | 55 | // 删除该文章 56 | $article->delete(); 57 | 58 | // 创建一个新文章 59 | $article = new Article(); 60 | $article->name = 'My first article'; 61 | $article->status = 'published'; 62 | // 将该文章保存到数据库中 63 | $article->insert(); 64 | ``` 65 | 66 | 大概在上面列了一下,我们简单的ORM实现之后的使用,据此我们可以定义出如下接口: 67 | 68 | ```php 69 | 55 | */ 56 | abstract class Application 57 | { 58 | /** 59 | * @var string the namespace that controller classes are located in. 60 | * This namespace will be used to load controller classes by prepending it to the controller class name. 61 | * The default namespace is `app\controllers`. 62 | */ 63 | public $controllerNamespace = 'app\\controllers'; 64 | 65 | /** 66 | * Runs the application. 67 | * This is the main entrance of an application. 68 | */ 69 | public function run() 70 | { 71 | try { 72 | return $this->handleRequest(); 73 | } catch (Exception $e) { 74 | return $e; 75 | } 76 | } 77 | 78 | /** 79 | * Handles the specified request. 80 | */ 81 | abstract public function handleRequest(); 82 | } 83 | 84 | ``` 85 | 86 | 它是一个抽象类,实现了一个简单的run方法,run方法就是去执行以下handleRequest方法。 87 | 88 | 它定义了一个抽象方法handleRequest,等待被继承,实现。 89 | 90 | 它定义了一个controllerNamespace属性,记录controller存放的namesapce,默认值是'app\\controllers'。 91 | 92 | 再来看在web里的Application.php 93 | 94 | ```php 95 | 101 | */ 102 | class Application extends \sf\base\Application 103 | { 104 | /** 105 | * Handles the specified request. 106 | * @return Response the resulting response 107 | */ 108 | public function handleRequest() 109 | { 110 | $router = $_GET['r']; 111 | list($controllerName, $actionName) = explode('/', $router); 112 | $ucController = ucfirst($controllerName); 113 | $controllerName = $this->controllerNamespace . '\\' . $ucController . 'Controller'; 114 | $controller = new $controllerName(); 115 | return call_user_func([$controller, 'action'. ucfirst($actionName)]); 116 | } 117 | } 118 | 119 | ``` 120 | 121 | 是不是觉得很眼熟,其实就是将之前放在index.php中的内容放到Application的handleRequest方法里了。 122 | 123 | 然后我们需要从入口文件调用到这里的代码,这就很简单了,index.php的内容如下: 124 | 125 | ```php 126 | run(); 131 | 132 | ``` 133 | 134 | 直接去new一个web Application的实例,执行run方法就可以了,是不是很简单。 135 | 136 | 访问 http://localhost/simple-framework/public/index.php?r=site/test ,你可以看到上一次一样的结果。 137 | 138 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 139 | 140 | code:[https://github.com/CraryPrimitiveMan/simple-framework/tree/0.2](https://github.com/CraryPrimitiveMan/simple-framework/tree/0.2) 141 | 142 | blog project:[https://github.com/CraryPrimitiveMan/create-your-own-php-framework](https://github.com/CraryPrimitiveMan/create-your-own-php-framework) 143 | -------------------------------------------------------------------------------- /12.md: -------------------------------------------------------------------------------- 1 | 自从来到新公司就一直很忙,最近这段时间终于稍微闲了一点,赶紧接着写这个系列,感觉再不写就烂尾了。 2 | 3 | 之前我们说到,拿到`{{ $name }}`这样一段内容时,我们只需要将它转化成``这样,就可以识别了,输出相应的变量值。 4 | 5 | 那就要需要正则匹配`{{ $name }}`,然后替换掉`{{`和`}}`,分别替换成``。 6 | 7 | 但是要想到一个问题,如果我在 view 里写了 php 的代码,其中含有`{{ $name }}`,也会被替换。例子如下: 8 | ```php 9 | 13 | ``` 14 | 15 | 要解决这个问题,我们需要将 PHP 的代码去掉,只留下 html 代码再做替换的处理。幸好 PHP 有一个方法 token_get_all,会将提供的内容按 PHP 标记进行分割。使用此方法解析如下内容: 16 | ```php 17 | $content = <<{{ $body }}
138 | 139 | 140 | ``` 141 | 142 | 然后我们来改造`Controller`中的`render`方法,代码如下 143 | 144 | ``` 145 | public function render($view, $params = []) 146 | { 147 | $file = '../views/' . $view . '.sf'; 148 | $fileContent = file_get_contents($file); 149 | $result = ''; 150 | foreach (token_get_all($fileContent) as $token) { 151 | if (is_array($token)) { 152 | list($id, $content) = $token; 153 | if ($id == T_INLINE_HTML) { 154 | $content = preg_replace('/{{(.*)}}/', '', $content); 155 | } 156 | $result .= $content; 157 | } else { 158 | $result .= $token; 159 | } 160 | } 161 | $generatedFile = '../runtime/cache/' . md5($file); 162 | file_put_contents($generatedFile, $result); 163 | extract($params); 164 | require_once $generatedFile; 165 | } 166 | ``` 167 | 168 | 修改`actionView`如下 169 | 170 | ```php 171 | public function actionView() 172 | { 173 | $this->render('site/view', ['body' => 'Test body information']); 174 | } 175 | ``` 176 | 177 | 访问 http://localhost/simple-framework/public/index.php?r=site/view ,得到如下页面 178 |  179 | 180 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 181 | 182 | code:https://github.com/CraryPrimitiveMan/simple-framework/tree/1.1 183 | 184 | blog project:https://github.com/CraryPrimitiveMan/create-your-own-php-framework 185 | -------------------------------------------------------------------------------- /01.md: -------------------------------------------------------------------------------- 1 | # 构建自己的PHP框架--搭建基本结构 2 | 3 | 首先,我们来说一下,为什么要创建自己的框架? 4 | 5 | 为什么要创建自己的框架呢?如果你跟周围的人讨论,每个人都会告诉你重复发明轮子是一件糟糕的事情,你最好选择一个已有的框架,忘掉"创建自己的框架"这种想法。大部分情况,他们是正确的,但是我想到了几个创建自己的框架的好处: 6 | 7 | * 了解更多框架的底层架构 8 | * 创建一个能满足你特殊需求的框架(但首先要确定你的需求真的是很特别) 9 | * 因为乐趣而试着写一个框架(为了"学习然后抛弃"的目的) 10 | * 想利用新的开发技术以及最佳实践重构已经存在的项目 11 | * 向世界证明自己也是可以写出框架的(......但只需那么一点点付出) 12 | 13 | 我将一步步的,循序渐进的引导你创建一个框架。每一步你得到的都是一个完全能使用的框架。我们将从一个简单的框架开始,然后一点点的给它加功能。最后,你将能得到一个完整的web框架。 14 | 15 | 上面的原因是摘来的[使用Symfony2的组件创建自己的PHP框架](http://www.chrisyue.com/translation-create-your-own-framework-on-top-of-the-symfony2-components-part-1.html),觉得说的不错就直接拿过来用了。 16 | 17 | 我们先建立一个目录,然后进入该目录 18 | 19 | ```shell 20 | mkdir simple-framework 21 | cd simple-framework 22 | ``` 23 | 24 | 然后分别建立放置controller/model/view等的目录 25 | 26 | ```shell 27 | mkdir controllers models views public 28 | ``` 29 | 30 | public用来存放统一的入口,在里面建立index.php,大部分框架现在都是单一入口。 31 | 32 | 然后我们需要支持以下composer,我们希望第三方的包都能通过composer来管理。如果你还不知道composer是什么,请查看[composer](http://www.phpcomposer.com/)。 33 | 34 | 在simple-framework文件夹下执行 composer init,然后填写相应内容,生成一个composer.json文件,其内容大概如下: 35 | 36 | ```json 37 | { 38 | "name": "craryprimitiveman/simple-framework", 39 | "description": "A simple php framework", 40 | "license": "MIT", 41 | "authors": [ 42 | { 43 | "name": "harrysun", 44 | "email": "sunguangjun@126.com" 45 | } 46 | ], 47 | "require": {} 48 | } 49 | ``` 50 | 51 | 让后修改以下,结果如下: 52 | 53 | ```json 54 | { 55 | "name": "craryprimitiveman/simple-framework", 56 | "description": "A simple php framework", 57 | "license": "MIT", 58 | "authors": [ 59 | { 60 | "name": "harrysun", 61 | "email": "sunguangjun@126.com" 62 | } 63 | ], 64 | "require": {}, 65 | "autoload": { 66 | "psr-4": { 67 | "sf\\": "src/", 68 | "app\\": "" 69 | } 70 | }, 71 | "repositories": [ 72 | {"type": "composer", "url": "http://packagist.phpcomposer.com"}, 73 | {"packagist": false} 74 | ] 75 | } 76 | ``` 77 | 78 | 其中的autoload是为了支持我们自己项目的文件加载,其中sf下的是framework的code,而app下的是正常业务罗辑的code,其中的repositories是为了解决在国内使用composer下载,下载不下来的问题,如果在国外,或者有VPN做代理,可以直接去掉。 79 | 80 | 然后执行composer install。 81 | 82 | 这样基本的目录结构就构建好了。 83 | 84 | 在入口文件public/index.php中,引入autoload文件,如下: 85 | 86 | ```php 87 | 140 |
141 |This is user {{ $user->id }}
43 | @endforeach 44 | 45 | @while (true) 46 |I'm looping forever.
47 | @endwhile 48 | ``` 49 | 50 | + 引入其他视图 51 | 52 | ```php 53 | @include('view.name', ['some' => 'data']) 54 | ``` 55 | 56 | 要匹配这些定义,我们要写出相应的正则表达式,关于`@`开头的命令直接拿了`laravel`中的使用。 57 | 58 | 我们先在`src`下创建`view`文件夹,再创建`Compiler`类文件。 59 | 60 | 我们将compiler的方式氛围两种,一种是`@`开头的命令(`Statements`),一种是输出(`Echos`)。这两种的正则是不一样的。 61 | 62 | 首先定义变量`compliers`,定义如下: 63 | 64 | ```php 65 | protected $compilers = [ 66 | 'Statements', 67 | 'Echos', 68 | ]; 69 | ``` 70 | 71 | 然后按着见原来`Controller`中`render`方法的内容迁移到`Complier`类中,按这两种依次匹配,代码如下: 72 | 73 | ```php 74 | public function compile($path = null) 75 | { 76 | $fileContent = file_get_contents($path); 77 | $result = ''; 78 | foreach (token_get_all($fileContent) as $token) { 79 | if (is_array($token)) { 80 | list($id, $content) = $token; 81 | if ($id == T_INLINE_HTML) { 82 | foreach ($this->compilers as $type) { 83 | $content = $this->{"compile{$type}"}($content); 84 | } 85 | } 86 | $result .= $content; 87 | } else { 88 | $result .= $token; 89 | } 90 | } 91 | $generatedFile = '../runtime/cache/' . md5($path); 92 | file_put_contents($generatedFile, $result); 93 | require_once $generatedFile; 94 | } 95 | 96 | protected function compileStatements($content) 97 | { 98 | return $content; 99 | } 100 | 101 | protected function compileEchos($content) 102 | { 103 | return preg_replace('/{{(.*)}}/', '', $content); 104 | } 105 | ``` 106 | 107 | 其中的`Statements`完全没有处理,`Echos`则还是跟之前一样。 108 | 109 | 先来调整下`Echos`中的处理,添加变量记录`{{ }}`和`{!! !!}`的名称 110 | 111 | ```php 112 | protected $echoCompilers = [ 113 | 'RawEchos', 114 | 'EscapedEchos' 115 | ]; 116 | ``` 117 | 118 | 处理的时候可以添加一下存在的判断,默认值是`null`,内容可以调整如下: 119 | 120 | ```php 121 | protected function compileEchos($content) 122 | { 123 | foreach ($this->echoCompilers as $type) { 124 | $content = $this->{"compile{$type}"}($content); 125 | } 126 | return $content; 127 | } 128 | 129 | protected function compileEscapedEchos($content) 130 | { 131 | return preg_replace('/{{(.*)}}/', '', $content); 132 | } 133 | 134 | protected function compileRawEchos($content) 135 | { 136 | return preg_replace('/{!!(.*)!!}/', '', $content); 137 | } 138 | ``` 139 | 140 | `EscapedEchos`和`RawEchos`的区别在于,第一个会做`html`转义。 141 | 142 | 我们再来看`Statements`命令的处理,其原理也一样,匹配到相应的命令,如`if`、`foreach`,调用相应的方法做替换。 143 | 144 | 代码如下: 145 | 146 | ```php 147 | protected function compileStatements($content) 148 | { 149 | return preg_replace_callback( 150 | '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', function ($match) { 151 | return $this->compileStatement($match); 152 | }, $content 153 | ); 154 | } 155 | 156 | protected function compileStatement($match) 157 | { 158 | if (strpos($match[1], '@') !== false) { 159 | $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1]; 160 | } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) { 161 | $match[0] = $this->$method(isset($match[3]) ? $match[3] : null); 162 | } 163 | 164 | return isset($match[3]) ? $match[0] : $match[0].$match[2]; 165 | } 166 | 167 | protected function compileIf($expression) 168 | { 169 | return ""; 170 | } 171 | 172 | protected function compileElseif($expression) 173 | { 174 | return ""; 175 | } 176 | 177 | protected function compileElse($expression) 178 | { 179 | return ""; 180 | } 181 | 182 | protected function compileEndif($expression) 183 | { 184 | return ''; 185 | } 186 | 187 | protected function compileFor($expression) 188 | { 189 | return ""; 190 | } 191 | 192 | protected function compileEndfor($expression) 193 | { 194 | return ''; 195 | } 196 | 197 | protected function compileForeach($expression) 198 | { 199 | return ""; 200 | } 201 | 202 | protected function compileEndforeach($expression) 203 | { 204 | return ''; 205 | } 206 | 207 | protected function compileWhile($expression) 208 | { 209 | return ""; 210 | } 211 | 212 | protected function compileEndwhile($expression) 213 | { 214 | return ''; 215 | } 216 | 217 | protected function compileContinue($expression) 218 | { 219 | return ''; 220 | } 221 | 222 | protected function compileBreak($expression) 223 | { 224 | return ''; 225 | } 226 | ``` 227 | 228 | 其中的`include`实现比较麻烦,就没有做,留给大家思考啦。 229 | 230 | 然后,我们再考虑一下,不可能每次都去操作文件重新生成,我应该要判断文件改变,如果没改变直接使用缓存就可以了。 231 | 232 | 调整代码如下: 233 | 234 | ```php 235 | public function isExpired($path) 236 | { 237 | $compiled = $this->getCompiledPath($path); 238 | if (!file_exists($compiled)) { 239 | return true; 240 | } 241 | 242 | return filemtime($path) >= filemtime($compiled); 243 | } 244 | 245 | protected function getCompiledPath($path) 246 | { 247 | return '../runtime/cache/' . md5($path); 248 | } 249 | 250 | public function compile($file = null, $params = []) 251 | { 252 | $path = '../views/' . $file . '.sf'; 253 | extract($params); 254 | if (!$this->isExpired($path)) { 255 | $compiled = $this->getCompiledPath($path); 256 | require_once $compiled; 257 | return; 258 | } 259 | $fileContent = file_get_contents($path); 260 | $result = ''; 261 | foreach (token_get_all($fileContent) as $token) { 262 | if (is_array($token)) { 263 | list($id, $content) = $token; 264 | if ($id == T_INLINE_HTML) { 265 | foreach ($this->compilers as $type) { 266 | $content = $this->{"compile{$type}"}($content); 267 | } 268 | } 269 | $result .= $content; 270 | } else { 271 | $result .= $token; 272 | } 273 | } 274 | $compiled = $this->getCompiledPath($path); 275 | file_put_contents($compiled, $result); 276 | require_once $compiled; 277 | } 278 | ``` 279 | 280 | 这个系列的博客到这里就暂时告一段落了~ 281 | 282 | 项目内容和博客内容也都会放到Github上,欢迎大家提建议。 283 | 284 | code:https://github.com/CraryPrimitiveMan/simple-framework/tree/1.2 285 | 286 | blog project:https://github.com/CraryPrimitiveMan/create-your-own-php-framework 287 | -------------------------------------------------------------------------------- /09.md: -------------------------------------------------------------------------------- 1 | 作为一个框架,我们还没有相应的缓存组件,下面我们就来构建我们的缓存组件。 2 | 3 | 先来定义一下接口,在 src 文件夹下创建 cache 文件夹,在cache文件夹下创建 CacheInterface.php 文件,其中定义 Cache 相应的接口,其内容如下: 4 | 5 | 6 | ```php 7 | 13 | */ 14 | interface CacheInterface 15 | { 16 | /** 17 | * Builds a normalized cache key from a given key. 18 | */ 19 | public function buildKey($key); 20 | 21 | /** 22 | * Retrieves a value from cache with a specified key. 23 | */ 24 | public function get($key); 25 | 26 | /** 27 | * Checks whether a specified key exists in the cache. 28 | */ 29 | public function exists($key); 30 | 31 | /** 32 | * Retrieves multiple values from cache with the specified keys. 33 | */ 34 | public function mget($keys); 35 | 36 | /** 37 | * Stores a value identified by a key into cache. 38 | */ 39 | public function set($key, $value, $duration = 0); 40 | 41 | /** 42 | * Stores multiple items in cache. Each item contains a value identified by a key. 43 | */ 44 | public function mset($items, $duration = 0); 45 | 46 | /** 47 | * Stores a value identified by a key into cache if the cache does not contain this key. 48 | * Nothing will be done if the cache already contains the key. 49 | */ 50 | public function add($key, $value, $duration = 0); 51 | 52 | /** 53 | * Stores multiple items in cache. Each item contains a value identified by a key. 54 | * If the cache already contains such a key, the existing value and expiration time will be preserved. 55 | */ 56 | public function madd($items, $duration = 0); 57 | 58 | /** 59 | * Deletes a value with the specified key from cache 60 | */ 61 | public function delete($key); 62 | 63 | /** 64 | * Deletes all values from cache. 65 | */ 66 | public function flush(); 67 | } 68 | ``` 69 | 70 | 定义了 buildKey/get/mget/set/mset/exists/add/madd/delete/flush接口,对应功能如下: 71 | 72 | + buildKey:构建真正的 key,避免特殊字符影响实现 73 | + get:根据 key 获取缓存的值 74 | + mget:根据 keys 数组获取多个缓存值 75 | + set:根据 key 设置缓存的值 76 | + mset:根据数组设置多个缓存值 77 | + exists:判断 key 是否存在 78 | + add:如果 key 不存在就设置缓存值,否则返回false 79 | + madd:根据数组,判断相应的 key 不存在就设置缓存值 80 | + delete:根据 key 删除一个缓存 81 | + flush:删除所有的缓存 82 | 83 | 实现缓存,可以使用很多方式,比如使用文件、数据库、memcache 以及 Redis 等。 84 | 85 | 我们今天先使用文件缓存来实现相应的接口。 86 | 87 | 其主要思想就是,每一个 key 都对应一个文件,缓存的内容序列化一下,存入到文件中,取出时再反序列化一下。剩下的基本都是相应的文件操作了。 88 | 89 | 在 src/cache 文件夹下创建 FileCache.php 文件,其内容如下: 90 | 91 | ```php 92 | 98 | */ 99 | class FileCache implements CacheInterface 100 | { 101 | /** 102 | * @var string the directory to store cache files. 103 | * 缓存文件的地址,例如/Users/jun/projects/www/simple-framework/runtime/cache/ 104 | */ 105 | public $cachePath; 106 | /** 107 | * Builds a normalized cache key from a given key. 108 | */ 109 | public function buildKey($key) 110 | { 111 | if (!is_string($key)) { 112 | // 不是字符串就json_encode一把,转成字符串,也可以用其他方法 113 | $key = json_encode($key); 114 | } 115 | return md5($key); 116 | } 117 | 118 | /** 119 | * Retrieves a value from cache with a specified key. 120 | */ 121 | public function get($key) 122 | { 123 | $key = $this->buildKey($key); 124 | $cacheFile = $this->cachePath . $key; 125 | // filemtime用来获取文件的修改时间 126 | if (@filemtime($cacheFile) > time()) { 127 | // file_get_contents用来获取文件内容,unserialize用来反序列化文件内容 128 | return unserialize(@file_get_contents($cacheFile)); 129 | } else { 130 | return false; 131 | } 132 | } 133 | 134 | /** 135 | * Checks whether a specified key exists in the cache. 136 | */ 137 | public function exists($key) 138 | { 139 | $key = $this->buildKey($key); 140 | $cacheFile = $this->cachePath . $key; 141 | // 用修改时间标记过期时间,存入时会做相应的处理 142 | return @filemtime($cacheFile) > time(); 143 | } 144 | 145 | /** 146 | * Retrieves multiple values from cache with the specified keys. 147 | */ 148 | public function mget($keys) 149 | { 150 | $results = []; 151 | foreach ($keys as $key) { 152 | $results[$key] = $this->get($key); 153 | } 154 | return $results; 155 | } 156 | 157 | /** 158 | * Stores a value identified by a key into cache. 159 | */ 160 | public function set($key, $value, $duration = 0) 161 | { 162 | $key = $this->buildKey($key); 163 | $cacheFile = $this->cachePath . $key; 164 | // serialize用来序列化缓存内容 165 | $value = serialize($value); 166 | // file_put_contents用来将序列化之后的内容写入文件,LOCK_EX表示写入时会对文件加锁 167 | if (@file_put_contents($cacheFile, $value, LOCK_EX) !== false) { 168 | if ($duration <= 0) { 169 | // 不设置过期时间,设置为一年,这是因为用文件的修改时间来做过期时间造成的 170 | // redis/memcache 等都不会有这个问题 171 | $duration = 31536000; // 1 year 172 | } 173 | // touch用来设置修改时间,过期时间为当前时间加上$duration 174 | return touch($cacheFile, $duration + time()); 175 | } else { 176 | return false; 177 | } 178 | } 179 | 180 | /** 181 | * Stores multiple items in cache. Each item contains a value identified by a key. 182 | */ 183 | public function mset($items, $duration = 0) 184 | { 185 | $failedKeys = []; 186 | foreach ($items as $key => $value) { 187 | if ($this->set($key, $value, $duration) === false) { 188 | $failedKeys[] = $key; 189 | } 190 | } 191 | 192 | return $failedKeys; 193 | } 194 | 195 | /** 196 | * Stores a value identified by a key into cache if the cache does not contain this key. 197 | */ 198 | public function add($key, $value, $duration = 0) 199 | { 200 | // key不存在,就设置缓存 201 | if (!$this->exists($key)) { 202 | return $this->set($key, $value, $duration); 203 | } else { 204 | return false; 205 | } 206 | } 207 | 208 | /** 209 | * Stores multiple items in cache. Each item contains a value identified by a key. 210 | */ 211 | public function madd($items, $duration = 0) 212 | { 213 | $failedKeys = []; 214 | foreach ($items as $key => $value) { 215 | if ($this->add($key, $value, $duration) === false) { 216 | $failedKeys[] = $key; 217 | } 218 | } 219 | 220 | return $failedKeys; 221 | } 222 | 223 | /** 224 | * Deletes a value with the specified key from cache 225 | */ 226 | public function delete($key) 227 | { 228 | $key = $this->buildKey($key); 229 | $cacheFile = $this->cachePath . $key; 230 | // unlink用来删除文件 231 | return unlink($cacheFile); 232 | } 233 | 234 | /** 235 | * Deletes all values from cache. 236 | * Be careful of performing this operation if the cache is shared among multiple applications. 237 | * @return boolean whether the flush operation was successful. 238 | */ 239 | public function flush() 240 | { 241 | // 打开cache文件所在目录 242 | $dir = @dir($this->cachePath); 243 | 244 | // 列出目录中的所有文件 245 | while (($file = $dir->read()) !== false) { 246 | if ($file !== '.' && $file !== '..') { 247 | unlink($this->cachePath . $file); 248 | } 249 | } 250 | 251 | // 关闭目录 252 | $dir->close(); 253 | } 254 | } 255 | ``` 256 | 257 | 相关实现的解释都直接写在code中的注释里了。 258 | 259 | 然后我们来测试一下我们的缓存组件,首先我们需要添加一下配置文件,在 config 文件夹下创建 cache.php 文件,配置如下内容: 260 | 261 | ```php 262 | '\sf\cache\FileCache', 265 | 'cachePath' => SF_PATH . '/runtime/cache/' 266 | ]; 267 | ``` 268 | 269 | 然后在 SiteController.php 中简单使用如下: 270 | 271 | ```php 272 | public function actionCache() 273 | { 274 | $cache = Sf::createObject('cache'); 275 | $cache->set('test', '我就是测试一下缓存组件'); 276 | $result = $cache->get('test'); 277 | $cache->flush(); 278 | echo $result; 279 | } 280 | ``` 281 | 282 | 访问 http://localhost/simple-framework/public/index.php?r=site/cache 路径,得到结果如下: 283 | 284 | ```sh 285 | 我就是测试一下缓存组件 286 | ``` 287 | 288 | 这样我们完成了使用文件的缓存组件。 289 | 290 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 291 | 292 | code:https://github.com/CraryPrimitiveMan/simple-framework/tree/0.9 293 | 294 | blog project:https://github.com/CraryPrimitiveMan/create-your-own-php-framework 295 | -------------------------------------------------------------------------------- /05.md: -------------------------------------------------------------------------------- 1 | 在之前的博客中,我们定义了ORM的接口,以及决定了使用PDO去实现。最后我们提到会有一个Model类实现ModelInterface接口。 2 | 3 | 现在我们来实现这个接口,如下: 4 | 5 | 6 | ```php 7 | 15 | */ 16 | class Model implements ModelInterface 17 | { 18 | /** 19 | * Declares the name of the database table associated with this Model class. 20 | * @return string the table name 21 | */ 22 | public static function tableName() 23 | { 24 | return get_called_class(); 25 | } 26 | 27 | /** 28 | * Returns the primary key **name(s)** for this Model class. 29 | * @return string[] the primary key name(s) for this Model class. 30 | */ 31 | public static function primaryKey() 32 | { 33 | return ['id']; 34 | } 35 | 36 | /** 37 | * Returns a single model instance by a primary key or an array of column values. 38 | * 39 | * // find the first customer whose age is 30 and whose status is 1 40 | * $customer = Customer::findOne(['age' => 30, 'status' => 1]); 41 | * 42 | * @param mixed $condition a set of column values 43 | * @return static|null Model instance matching the condition, or null if nothing matches. 44 | */ 45 | public static function findOne($condition) 46 | { 47 | 48 | } 49 | 50 | /** 51 | * Returns a list of models that match the specified primary key value(s) or a set of column values. 52 | * 53 | * // find customers whose age is 30 and whose status is 1 54 | * $customers = Customer::findAll(['age' => 30, 'status' => 1]); 55 | * 56 | * @param mixed $condition a set of column values 57 | * @return array an array of Model instance, or an empty array if nothing matches. 58 | */ 59 | public static function findAll($condition) 60 | { 61 | 62 | } 63 | 64 | /** 65 | * Updates models using the provided attribute values and conditions. 66 | * For example, to change the status to be 2 for all customers whose status is 1: 67 | * 68 | * Customer::updateAll(['status' => 1], ['status' => '2']); 69 | * 70 | * @param array $attributes attribute values (name-value pairs) to be saved for the model. 71 | * @param array $condition the condition that matches the models that should get updated. 72 | * An empty condition will match all models. 73 | * @return integer the number of rows updated 74 | */ 75 | public static function updateAll($condition, $attributes) 76 | { 77 | 78 | } 79 | 80 | /** 81 | * Deletes models using the provided conditions. 82 | * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. 83 | * 84 | * For example, to delete all customers whose status is 3: 85 | * 86 | * Customer::deleteAll([status = 3]); 87 | * 88 | * @param array $condition the condition that matches the models that should get deleted. 89 | * An empty condition will match all models. 90 | * @return integer the number of rows deleted 91 | */ 92 | public static function deleteAll($condition) 93 | { 94 | 95 | } 96 | 97 | /** 98 | * Inserts the model into the database using the attribute values of this record. 99 | * 100 | * Usage example: 101 | * 102 | * $customer = new Customer; 103 | * $customer->name = $name; 104 | * $customer->email = $email; 105 | * $customer->insert(); 106 | * 107 | * @return boolean whether the model is inserted successfully. 108 | */ 109 | public function insert() 110 | { 111 | 112 | } 113 | 114 | /** 115 | * Saves the changes to this model into the database. 116 | * 117 | * Usage example: 118 | * 119 | * $customer = Customer::findOne(['id' => $id]); 120 | * $customer->name = $name; 121 | * $customer->email = $email; 122 | * $customer->update(); 123 | * 124 | * @return integer|boolean the number of rows affected. 125 | * Note that it is possible that the number of rows affected is 0, even though the 126 | * update execution is successful. 127 | */ 128 | public function update() 129 | { 130 | 131 | } 132 | 133 | /** 134 | * Deletes the model from the database. 135 | * 136 | * @return integer|boolean the number of rows deleted. 137 | * Note that it is possible that the number of rows deleted is 0, even though the deletion execution is successful. 138 | */ 139 | public function delete() 140 | { 141 | 142 | } 143 | } 144 | ``` 145 | 146 | 当然现在里面还没有写任何的实现,只是继承了ModelInterface接口。 147 | 148 | 现在我们先来实现一下findOne方法,在开始实现之前我们要想,我们所有的model都要基于PDO,所以我们应该在model中有一个PDO的实例。所以我们需要在Model类中添加如下变量和方法。 149 | 150 | ```php 151 | /** 152 | * @var $pdo PDO instance 153 | */ 154 | public static $pdo; 155 | 156 | /** 157 | * Get pdo instance 158 | * @return PDO 159 | */ 160 | public static function getDb() 161 | { 162 | if (empty(static::$pdo)) { 163 | $host = 'localhost'; 164 | $database = 'sf'; 165 | $username = 'jun'; 166 | $password = 'jun'; 167 | static::$pdo = new PDO("mysql:host=$host;dbname=$database", $username, $password); 168 | static::$pdo->exec("set names 'utf8'"); 169 | } 170 | 171 | return static::$pdo; 172 | } 173 | ``` 174 | 175 | 用static变量可以保证所有继承该Model的类用的都是同一个PDO实例,getDb方法实现了单例模式(其中的配置暂时hard在这里,在之后的博客里会抽出来),保证了一个请求中,使用getDb只会取到一个PDO实例。 176 | 177 | 下面我们来实现findOne方法,我们现在定义的findOne有很多局限,例如不支持or,不支持select部分字段,不支持表关联等等。我们之后会慢慢完善这一些内容。其实findOne的实现就是拼接sql语句去执行,直接来看下代码: 178 | 179 | ```php 180 | public static function findOne($condition) 181 | { 182 | // 拼接默认的前半段sql语句 183 | $sql = 'select * from ' . static::tableName() . ' where '; 184 | // 取出condition中value作为参数 185 | $params = array_values($condition); 186 | $keys = []; 187 | foreach ($condition as $key => $value) { 188 | array_push($keys, "$key = ?"); 189 | } 190 | // 拼接sql完成 191 | $sql .= implode(' and ', $keys); 192 | $stmt = static::getDb()->prepare($sql); 193 | $rs = $stmt->execute($params); 194 | 195 | if ($rs) { 196 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 197 | if (!empty($row)) { 198 | // 创建相应model的实例 199 | $model = new static(); 200 | foreach ($row as $rowKey => $rowValue) { 201 | // 给model的属性赋值 202 | $model->$rowKey = $rowValue; 203 | } 204 | return $model; 205 | } 206 | } 207 | // 默认返回null 208 | return null; 209 | } 210 | ``` 211 | 212 | 我们需要来验证一下我们的代码是正确的,先在MySQL中设置一下用户及权限,mock一下数据。 213 | 214 | 相应的SQL语句如下: 215 | 216 | ```sql 217 | /*创建新用户*/ 218 | CREATE USER jun@localhost IDENTIFIED BY 'jun'; 219 | 220 | /*用户授权 授权jun用户拥有sf数据库的所有权限*/ 221 | GRANT ALL PRIVILEGES ON sf.* TO jun@'%' IDENTIFIED BY 'jun'; 222 | 223 | /*刷新授权*/ 224 | FLUSH PRIVILEGES; 225 | 226 | /*创建数据库*/ 227 | CREATE DATABASE IF NOT EXISTS `sf`; 228 | 229 | /*选择数据库*/ 230 | USE `sf`; 231 | 232 | /*创建表*/ 233 | CREATE TABLE IF NOT EXISTS `user` ( 234 | id INT(20) NOT NULL AUTO_INCREMENT, 235 | name VARCHAR(50), 236 | age INT(11), 237 | PRIMARY KEY(id) 238 | ); 239 | 240 | /*插入测试数据*/ 241 | INSERT INTO `user` (name, age) VALUES('harry', 20), ('tony', 23), ('tom', 24); 242 | 243 | ``` 244 | 245 | 然后再在models文件夹中创建一个User.php,代码如下: 246 | 247 | ```php 248 | 20, 'name' => 'harry']); 274 | $data = [ 275 | 'first' => 'awesome-php-zh_CN', 276 | 'second' => 'simple-framework', 277 | 'user' => $user 278 | ]; 279 | echo $this->toJson($data); 280 | } 281 | ``` 282 | 打出的如下结果: 283 | 284 | ```json 285 | {"first":"awesome-php-zh_CN","second":"simple-framework","user":{"id":"1","name":"harry","age":"20"}} 286 | ``` 287 | 288 | 如果将findOne中的条件改成['age' => 20, 'name' => 'tom'],返回值就变为如下结果: 289 | 290 | ```json 291 | {"first":"awesome-php-zh_CN","second":"simple-framework","user":null} 292 | ``` 293 | 294 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 295 | 296 | code:[https://github.com/CraryPrimitiveMan/simple-framework/tree/0.4](https://github.com/CraryPrimitiveMan/simple-framework/tree/0.4) 297 | 298 | blog project:[https://github.com/CraryPrimitiveMan/create-your-own-php-framework](https://github.com/CraryPrimitiveMan/create-your-own-php-framework) 299 | -------------------------------------------------------------------------------- /07.md: -------------------------------------------------------------------------------- 1 | 在之前的博客中,我们实现并完善了Model类的findOne方法,下面我们来实现其中的其他方法。 2 | 3 | 先来看findAll方法,这个方法和findOne很相似。 4 | 5 | ```php 6 | public static function findOne($condition = null) 7 | { 8 | $sql = 'select * from ' . static::tableName(); 9 | $params = []; 10 | 11 | // 判空 12 | if (!empty($condition)) { 13 | $sql .= ' where '; 14 | $params = array_values($condition); 15 | $keys = []; 16 | foreach ($condition as $key => $value) { 17 | array_push($keys, "$key = ?"); 18 | } 19 | $sql .= implode(' and ', $keys); 20 | } 21 | 22 | $stmt = static::getDb()->prepare($sql); 23 | $rs = $stmt->execute($params); 24 | $models = []; 25 | 26 | if ($rs) { 27 | // 直接获取出所有符合条件的 28 | $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); 29 | foreach ($rows as $row) { 30 | if (!empty($row)) { 31 | $model = new static(); 32 | foreach ($row as $rowKey => $rowValue) { 33 | $model->$rowKey = $rowValue; 34 | } 35 | array_push($models, $model); 36 | } 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | ``` 43 | 44 | 你会发现有findOne和findAll方法很相似,明显可以将公共的部分抽出来,然后我们就多了如下两个方法: 45 | 46 | ```php 47 | /** 48 | * Build a sql where part 49 | * @param mixed $condition a set of column values 50 | * @return string 51 | */ 52 | public static function buildWhere($condition, $params = null) 53 | { 54 | if (is_null($params)) { 55 | $params = []; 56 | } 57 | 58 | $where = ''; 59 | if (!empty($condition)) { 60 | $where .= ' where '; 61 | $keys = []; 62 | foreach ($condition as $key => $value) { 63 | array_push($keys, "$key = ?"); 64 | array_push($params, $value); 65 | } 66 | $where .= implode(' and ', $keys); 67 | } 68 | return [$where, $params]; 69 | } 70 | 71 | /** 72 | * Convert array to model 73 | * @param mixed $row the row data from database 74 | */ 75 | public static function arr2Model($row) 76 | { 77 | $model = new static(); 78 | foreach ($row as $rowKey => $rowValue) { 79 | $model->$rowKey = $rowValue; 80 | } 81 | return $model; 82 | } 83 | ``` 84 | 85 | 分别是构建sql中where部分的方法和将查找到的Array转换成Model的方法。大家会奇怪第一个方法中为什么需要params参数和返回值,其实这个为了之后的updateAll方法的使用。其实这个地方跟适合使用引用传值。 86 | 87 | 这样我们的findOne和findAll就便成了如下内容: 88 | 89 | ```php 90 | /** 91 | * Returns a single model instance by a primary key or an array of column values. 92 | * 93 | * // find the first customer whose age is 30 and whose status is 1 94 | * $customer = Customer::findOne(['age' => 30, 'status' => 1]); 95 | * 96 | * @param mixed $condition a set of column values 97 | * @return static|null Model instance matching the condition, or null if nothing matches. 98 | */ 99 | public static function findOne($condition = null) 100 | { 101 | list($where, $params) = static::buildWhere($condition); 102 | $sql = 'select * from ' . static::tableName() . $where; 103 | 104 | $stmt = static::getDb()->prepare($sql); 105 | $rs = $stmt->execute($params); 106 | 107 | if ($rs) { 108 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 109 | if (!empty($row)) { 110 | return static::arr2Model($row); 111 | } 112 | } 113 | 114 | return null; 115 | } 116 | 117 | /** 118 | * Returns a list of models that match the specified primary key value(s) or a set of column values. 119 | * 120 | * // find customers whose age is 30 and whose status is 1 121 | * $customers = Customer::findAll(['age' => 30, 'status' => 1]); 122 | * 123 | * @param mixed $condition a set of column values 124 | * @return array an array of Model instance, or an empty array if nothing matches. 125 | */ 126 | public static function findAll($condition = null) 127 | { 128 | list($where, $params) = static::buildWhere($condition); 129 | $sql = 'select * from ' . static::tableName() . $where; 130 | 131 | $stmt = static::getDb()->prepare($sql); 132 | $rs = $stmt->execute($params); 133 | $models = []; 134 | 135 | if ($rs) { 136 | $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); 137 | foreach ($rows as $row) { 138 | if (!empty($row)) { 139 | $model = static::arr2Model($row); 140 | array_push($models, $model); 141 | } 142 | } 143 | } 144 | 145 | return $models; 146 | } 147 | ``` 148 | 149 | 剩下的updateAll/deleteAll/insert/update和delete方法就不一一详细说明了,直接给出代码。其基本思想都是一致的,都是按照规则拼接SQL语句。 150 | 151 | ```php 152 | /** 153 | * Updates models using the provided attribute values and conditions. 154 | * For example, to change the status to be 2 for all customers whose status is 1: 155 | * 156 | * Customer::updateAll(['status' => 1], ['status' => '2']); 157 | * 158 | * @param array $attributes attribute values (name-value pairs) to be saved for the model. 159 | * @param array $condition the condition that matches the models that should get updated. 160 | * An empty condition will match all models. 161 | * @return integer the number of rows updated 162 | */ 163 | public static function updateAll($condition, $attributes) 164 | { 165 | $sql = 'update ' . static::tableName(); 166 | $params = []; 167 | 168 | if (!empty($attributes)) { 169 | $sql .= ' set '; 170 | $params = array_values($attributes); 171 | $keys = []; 172 | foreach ($attributes as $key => $value) { 173 | array_push($keys, "$key = ?"); 174 | } 175 | $sql .= implode(' , ', $keys); 176 | } 177 | 178 | list($where, $params) = static::buildWhere($condition, $params); 179 | $sql .= $where; 180 | 181 | $stmt = static::getDb()->prepare($sql); 182 | $execResult = $stmt->execute($params); 183 | if ($execResult) { 184 | // 获取更新的行数 185 | $execResult = $stmt->rowCount(); 186 | } 187 | return $execResult; 188 | } 189 | 190 | /** 191 | * Deletes models using the provided conditions. 192 | * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. 193 | * 194 | * For example, to delete all customers whose status is 3: 195 | * 196 | * Customer::deleteAll([status = 3]); 197 | * 198 | * @param array $condition the condition that matches the models that should get deleted. 199 | * An empty condition will match all models. 200 | * @return integer the number of rows deleted 201 | */ 202 | public static function deleteAll($condition) 203 | { 204 | list($where, $params) = static::buildWhere($condition); 205 | $sql = 'delete from ' . static::tableName() . $where; 206 | 207 | $stmt = static::getDb()->prepare($sql); 208 | $execResult = $stmt->execute($params); 209 | if ($execResult) { 210 | // 获取删除的行数 211 | $execResult = $stmt->rowCount(); 212 | } 213 | return $execResult; 214 | } 215 | 216 | /** 217 | * Inserts the model into the database using the attribute values of this record. 218 | * 219 | * Usage example: 220 | * 221 | * $customer = new Customer; 222 | * $customer->name = $name; 223 | * $customer->email = $email; 224 | * $customer->insert(); 225 | * 226 | * @return boolean whether the model is inserted successfully. 227 | */ 228 | public function insert() 229 | { 230 | $sql = 'insert into ' . static::tableName(); 231 | $params = []; 232 | $keys = []; 233 | foreach ($this as $key => $value) { 234 | array_push($keys, $key); 235 | array_push($params, $value); 236 | } 237 | // 构建由?组成的数组,其个数与参数相等数相同 238 | $holders = array_fill(0, count($keys), '?'); 239 | $sql .= ' (' . implode(' , ', $keys) . ') values ( ' . implode(' , ', $holders) . ')'; 240 | 241 | $stmt = static::getDb()->prepare($sql); 242 | $execResult = $stmt->execute($params); 243 | // 将一些自增值赋回Model中 244 | $primaryKeys = static::primaryKey(); 245 | foreach ($primaryKeys as $name) { 246 | // Get the primary key 247 | $lastId = static::getDb()->lastInsertId($name); 248 | $this->$name = (int) $lastId; 249 | } 250 | return $execResult; 251 | } 252 | 253 | /** 254 | * Saves the changes to this model into the database. 255 | * 256 | * Usage example: 257 | * 258 | * $customer = Customer::findOne(['id' => $id]); 259 | * $customer->name = $name; 260 | * $customer->email = $email; 261 | * $customer->update(); 262 | * 263 | * @return integer|boolean the number of rows affected. 264 | * Note that it is possible that the number of rows affected is 0, even though the 265 | * update execution is successful. 266 | */ 267 | public function update() 268 | { 269 | $primaryKeys = static::primaryKey(); 270 | $condition = []; 271 | foreach ($primaryKeys as $name) { 272 | $condition[$name] = isset($this->$name) ? $this->$name : null; 273 | } 274 | 275 | $attributes = []; 276 | foreach ($this as $key => $value) { 277 | if (!in_array($key, $primaryKeys, true)) { 278 | $attributes[$key] = $value; 279 | } 280 | } 281 | 282 | return static::updateAll($condition, $attributes) !== false; 283 | } 284 | 285 | /** 286 | * Deletes the model from the database. 287 | * 288 | * @return integer|boolean the number of rows deleted. 289 | * Note that it is possible that the number of rows deleted is 0, even though the deletion execution is successful. 290 | */ 291 | public function delete() 292 | { 293 | $primaryKeys = static::primaryKey(); 294 | $condition = []; 295 | foreach ($primaryKeys as $name) { 296 | $condition[$name] = isset($this->$name) ? $this->$name : null; 297 | } 298 | 299 | return static::deleteAll($condition) !== false; 300 | } 301 | ``` 302 | 303 | 这样基本的Model就算是暂时完成了,虽然可能还有很多问题和局限,但暂时先这样了,我们之后有机会会一步一步的去完善。 304 | 305 | 好了,今天就先到这里。项目内容和博客内容也都会放到Github上,欢迎大家提建议。 306 | 307 | code:[https://github.com/CraryPrimitiveMan/simple-framework/tree/0.7](https://github.com/CraryPrimitiveMan/simple-framework/tree/0.7) 308 | 309 | blog project:[https://github.com/CraryPrimitiveMan/create-your-own-php-framework](https://github.com/CraryPrimitiveMan/create-your-own-php-framework) 310 | --------------------------------------------------------------------------------