前言
终于有那么点时间能将Laravel 5
的一些好的实践总结出来,希望为普及Laravel
和新的PHP
编程思想出一份力。如有错误或你有更好的方式,请不吝赐教,共同进步。
本文有配套的git
仓库,你也可以clone
我的代码仓库,里面包含每一步的操作(没有多余步骤)。
git clone git@git.oschina.net:notfound/Separated-Laravel.git
正文
通常很多项目都会依赖同一个的框架,还会共用很多代码库,手动复制粘贴这些文件到每个项目文件夹显然很伤害键盘,特别当项目多了之后,手动管理各种不同版本的库极容易精神分裂。为避免让搬砖这项工作对身体、精神造成双重伤害,最好将这些文件公用化。那么有哪些方法呢?
使用Composer
Composer
是什么?Composer
是PHP
库的管理工具。简单来说就是所有库都要告诉Composer
自己依赖哪些库,这样当你告诉Composer
你需要哪些库(甚至特定的版本)的时候,Composer
就可以把你指定的库以及他们的依赖帮你全部下载到项目中。
很多人都用过老版本的ThinkPHP
或者CodeIgniter
,那么一定对import
、vendor
和$this->load
这些函数记忆犹新,他们经常在构造方法里成群出现,形成一道靓丽的风景线。那些日子可以忘掉了。使用Composer
,只要遵循PSR-0
、PSR-4
规范,即可实现自动加载。
Laravel
官方倡导使用Composer
来管理项目(新建Laravel
项目都是用的Composer
,让很多人感到不适应)。使用Composer
只需在项目目录下的composer.json
文件中注明依赖库的名字、版本,一个composer install
命令即可自动下载,并且这些库自身的依赖也会被自动处理。以下是一个典型Laravel 5
新项目的composer.json
:
{ "name": "laravel/laravel", "description": "The Laravel Framework.", "keywords": ["framework", "laravel"], "license": "MIT", "type": "project", "require": { "laravel/framework": "5.0.*" }, "require-dev": { "phpunit/phpunit": "~4.0", "phpspec/phpspec": "~2.1" }, "autoload": { "classmap": [ "database" ], "psr-4": { "App\\": "app/" } }, "autoload-dev": { "classmap": [ "tests/TestCase.php" ] }, "scripts": { "post-install-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-update-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-create-project-cmd": [ "php -r \"copy('.env.example', '.env');\"", "php artisan key:generate" ] }, "config": { "preferred-install": "dist" } }
这里就不细说Composer
的使用方法和配置格式了。
只需要理解一点,composer.json
就是你项目所需要的库的清单(Laravel
框架也是一个Composer
库),composer install
命令则会查找当前目录下的清单,然后自动下载这些库到当前目录的vendor/
文件夹(如果本地已经下载过相同版本,则直接从缓存读取),并且生成一个autoload.php
文件,然后你只需要require
这个文件,即可调用安装的库了(autoload.php
里实现了一个懒加载,在调用未声明类时,会按照自己的规则去引用这个文件)。
注意,在一个典型的Laravel
项目中,你并不需要手动require
这个文件,入口文件引用的/bootstrap/autoload.php
里已经包含。
简单介绍了Composer
,相信已经有人想到怎么用Composer
来管理公用代码:只需将所有用到的公用代码封装成库就行了。如果不想提交到Composer
官方的源,我们也可以在内网搭建一个公用库的Composer
服务器。
每个提交都会产生一个Composer
版本(便于管理),Composer
如果检测到本地有相同版本的缓存文件,安装速度也会非常快,不必太担心速度问题。但这个方法显得稍繁琐了。在修改库之后必须通过Composer
更新到源,接着依赖的项目还得一个个执行composer update
来更新(你可以写自动化脚本,不过就更麻烦了吧)。
在一个中小型项目中,我们可能只会维护一套框架版本(例如Laravel 5.*
)和一套公用代码库,那么在每个项目中都安装一次Laravel
框架和代码库总让人觉得有点不对劲。而且Composer
的官方源因为某些神秘原因而非常慢,有时新建一个Laravel
项目需要20分钟……我们不想浪费团队每个人的时间,我们试试有没有别的解决方案。
链接法
这是我总结的一套方案,目前工作得还不错。在构建共用库目录结构之前,我们得先把Laravel
框架公用出来,因为我们只需要一套能公用的框架代码。
我们先从一个标准Laravel
项目中分离出Laravel
框架。我知道有些人表示很担心,所以首先确定几个基本原则:
- 不改变
Laravel
项目的目录结构、不要改动框架代码,方便未来升级; - 不用奇怪的
hack
方式实现(通用性不强); - 不会给新建项目带来一些配置麻烦(比如得通过
ln -s
映射一些目录); - 没有任何功能遗失(我们想感受
Laravel
所有的优点)。
OK,明确了基本原则,我们来看看设想的、分离之后的目录结构:
application/ laravel/
application
是项目目录,laravel
是Laravel
框架的目录,清晰明了。
我们再来看看一个官方Laravel
项目的目录结构(使用Laravel 5.0.16
):
Laravel官方新项目结构
app/ bootstrap/ config/ database/ public/ resources/ storage/ tests/ vendor/ .env .env.example .gitattributes .gitignore artisan composer.json composer.lock gulpfile.js package.json phpspec.yml phpunit.xml readme.md server.php
如果你在使用ThinkPHP
、CodeIgniter
等没有采用composer
等技术的框架,看到这么多不认识的文件肯定不开心了……不过不要紧,这并不妨碍构建一个基础Hello world
实例(当然还是得下载Composer
),其他的东西你可以搜索网络了解,或者看我以后的教程分享(如果有时间写的话)。
文件夹和文件看起来很多,我们一个个来看吧。要分离出框架,首先我们要弄清楚什么是不能分离出去、必须放在项目文件夹里的。
不能分离的文件、目录
这是一份我总结的列表和原因:
app/ #项目的程序逻辑总不能拿出去吧? bootstrap/ #我们稍后单独说 config/ #项目配置,你懂的 database/ #项目的数据库相关脚本 public/ #项目的,入口文件`index.php`我们单独说 resources/ #项目的资源 storage/ #项目的本地存储 tests/ #项目的测试脚本,删掉也不影响 vendor/ #稍后单独说 .env #也是项目的配置,在`Laravel`文档中有说明 .env.example #是上面文件的好基友 .gitattributes #框架的,移走 .gitignore #框架的,移走 artisan #稍后单独说 composer.json #稍后单独说 composer.lock #稍后单独说 gulpfile.js #项目的,不细说,删掉也不影响 package.json #项目的,不细说,删掉也不影响 phpspec.yml #项目的,不细说,删掉也不影响 phpunit.xml #项目的,不细说,删掉也不影响 readme.md #框架的README,移走 server.php #稍后单独说
我们已经排除了一大半不能移动的文件(文件夹)。我们来单独看几个特殊的。
artisan
artisan
是Laravel
的特色之一,如果想要在项目目录执行php artisan [command]
,这个得保留。打开看看它的代码:
#!/usr/bin/env php <?php /* |-------------------------------------------------------------------------- | Register The Auto Loader |-------------------------------------------------------------------------- | | Composer provides a convenient, automatically generated class loader | for our application. We just need to utilize it! We'll require it | into the script here so that we do not have to worry about the | loading of any our classes "manually". Feels great to relax. | */ require __DIR__.'/bootstrap/autoload.php'; $app = require_once __DIR__.'/bootstrap/app.php'; /* |-------------------------------------------------------------------------- | Run The Artisan Application |-------------------------------------------------------------------------- | | When we run the console application, the current CLI command will be | executed in this console and the response sent back to a terminal | or another output device for the developers. Here goes nothing! | */ $kernel = $app->make('Illuminate\Contracts\Console\Kernel'); $status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput ); /* |-------------------------------------------------------------------------- | Shutdown The Application |-------------------------------------------------------------------------- | | Once Artisan has finished running. We will fire off the shutdown events | so that any final work may be done by the application before we shut | down the process. This is the last thing to happen to the request. | */ $kernel->terminate($input, $status); exit($status);
基本来说它就是一个入口文件,将处理逻辑丢给了Laravel
核心,所以它基本不会改变,我们可以放心留下它(require
路径问题我们后面接着说)。
server.php
<?php /** * Laravel - A PHP Framework For Web Artisans * * @package Laravel * @author Taylor Otwell <taylorotwell@gmail.com> */ $uri = urldecode( parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ); // This file allows us to emulate Apache's "mod_rewrite" functionality from the // built-in PHP web server. This provides a convenient way to test a Laravel // application without having installed a "real" web server software here. if ($uri !== '/' and file_exists(__DIR__.'/public'.$uri)) { return false; } require_once __DIR__.'/public/index.php';
使用PHP
内置web服务器启动这个脚本可以进行快速调试(无需Http服务器),文件结构一目了然,无需更改。
readme.md
放在laravel
文件夹,饮水思源,尊重劳动成果。
Composer文件、bootstrap/、vendor、public/index.php
这几个部分涉及了整个框架的加载流程,所以放在一起说。
分离Laravel
框架,我们得知道Laravel
框架在哪吧。既然是通过Composer
安装的,那肯定在vendor/
文件夹,我们把它移到我们自己的laravel/
文件夹不就完了!然而这并没有什么用……
当我们打开vendor/
,发现:
bin/ classpreloader/ composer/ danielstjules/ dnoegel/ doctrine/ ... symfony/ vlucas/ autoload.php
怎么这么多文件!可是composer.json
的require
部分明明是这样:
... "require": { "laravel/framework": "5.0.*" }, "require-dev": { "phpunit/phpunit": "~4.0", "phpspec/phpspec": "~2.1" }, ...
没错,这是一个官方Laravel
项目的依赖列表,除开Composer
自身产生的文件(composer/
和autoload.php
),应该只有3个目录才对,其他的是什么呢?
其实,其他的文件夹是项目依赖的依赖,Composer
默认都会放到顶层的vendor/
文件夹(和拖家带口的npm
的明显差别)。
那我们是不是把这些文件夹全部移到我们的laravel/
文件夹就行了呢?且慢。
Composer
我们继续看看根目录的composer.json
文件。
... "require-dev": { "phpunit/phpunit": "~4.0", "phpspec/phpspec": "~2.1" }, ...
Laravel
默认自带了phpspec.yml
和phpunit.xml
,两者都是代码测试工具的配置文件,所以默认也带上了这两个开发依赖(不影响项目正常运行的依赖)。Laravel
官方还有一些扩展包,也是通过Composer
安装的,更有Laravel
开发者喜闻乐见的ide-helper
(一个为Facade
特性增加代码补全功能的库),都需要通过Composer
安装。特别不要忘了,我们的Laravel
还要通过Composer
来升级啊,所以我们最好保留Composer
需要的结构,所以现在我们的laravel/
文件夹是这样的:
laravel/ bootstrap/ vendor/ composer.json composer.lock .gitattributes .gitignore README.md
我们将vendor/
和composer.json
原样保存,在项目中只需要引入vendor/autoload.php
就可以自动加载框架了,这样无论是升级Laravel
还是composer install
安装任何需要共用的包都非常容易。
但是请注意composer.json
的这一段:
... "autoload": { "classmap": [ "database" ], "psr-4": { "App\\": "app/" } }, "autoload-dev": { "classmap": [ "tests/TestCase.php" ] }, "scripts": { "post-install-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-update-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-create-project-cmd": [ "php -r \"copy('.env.example', '.env');\"", "php artisan key:generate" ] }, ...
这里都是针对项目的配置,不删掉会造成报错。那么我们改成:
... "autoload": { "classmap": [ ], "psr-4": { } }, "autoload-dev": { "classmap": [ ] }, "scripts": { "post-install-cmd": [ ], "post-update-cmd": [ ], "post-create-project-cmd": [ ] }, ...
我们的项目文件也需要依赖Composer
来实现例如自动加载
等功能,所以我们在application/
文件夹下创建一个新的composer.json
文件,内容如下:
{ "name": "application", "description": "my application.", "keywords": [], "license": "MIT", "type": "project", "require": { }, "require-dev": { }, "autoload": { "classmap": [ "database" ], "psr-4": { "App\\": "app/" } }, "autoload-dev": { "classmap": [ "tests/TestCase.php" ] }, "scripts": { "post-install-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-update-cmd": [ "php artisan clear-compiled", "php artisan optimize" ], "post-create-project-cmd": [ "php -r \"copy('.env.example', '.env');\"", "php artisan key:generate" ] }, "config": { "preferred-install": "dist" } }
接着在application/
目录中执行composer dumpautoload
以生成自动加载的相关文件。
public/index.php
和bootstrap/
大块头都移走了,我们再从入口文件开始看:
<?php /** * Laravel - A PHP Framework For Web Artisans * * @package Laravel * @author Taylor Otwell <taylorotwell@gmail.com> */ /* |-------------------------------------------------------------------------- | Register The Auto Loader |-------------------------------------------------------------------------- | | Composer provides a convenient, automatically generated class loader for | our application. We just need to utilize it! We'll simply require it | into the script here so that we don't have to worry about manual | loading any of our classes later on. It feels nice to relax. | */ require __DIR__.'/../bootstrap/autoload.php'; /* |-------------------------------------------------------------------------- | Turn On The Lights |-------------------------------------------------------------------------- | | We need to illuminate PHP development, so let us turn on the lights. | This bootstraps the framework and gets it ready for use, then it | will load up this application so that we can run it and send | the responses back to the browser and delight our users. | */ $app = require_once __DIR__.'/../bootstrap/app.php'; /* |-------------------------------------------------------------------------- | Run The Application |-------------------------------------------------------------------------- | | Once we have the application, we can simply call the run method, | which will execute the request and send the response back to | the client's browser allowing them to enjoy the creative | and wonderful application we have prepared for them. | */ $kernel = $app->make('Illuminate\Contracts\Http\Kernel'); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); $kernel->terminate($request, $response);
代码意图很明确,加载bootstrap/
下的两个文件,分别实现自动加载(懒加载)
和设置框架
,在public/index.php
的最后,启动了框架流程。这两个文件我们也移到laravel/bootstrap/
文件夹,不过需要解决一下路径问题。
例如bootstrap/autoload.php
文件里是这样的:
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Composer Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Include The Compiled Class File
|--------------------------------------------------------------------------
|
| To dramatically increase your application's performance, you may use a
| compiled class file which contains all of the classes commonly used
| by a request. The Artisan "optimize" is used to create this file.
|
*/
$compiledPath = __DIR__.'/../storage/framework/compiled.php';
if (file_exists($compiledPath))
{
require $compiledPath;
}
第30
行指定了storage/framework/compiled.php
(编译命令产生的缓存文件,用来提高性能),storage/
文件夹是属于项目的,那么我们在public/index.php
里定义一个项目文件夹路径:
// 项目文件夹 define('APP_DIR', __DIR__);
然后将bootstrap/autoload.php
的30
行改为:
$compiledPath = APP_DIR.'/../storage/framework/compiled.php';
完美。bootstrap/
下的文件并不涉及到Laravel
核心逻辑,我也不认为在5.*
版本(起码也是5.1
以内)中会有太大变化,所以放心改。我们再看看bootstrap/app.php
:
<?php /* |-------------------------------------------------------------------------- | Create The Application |-------------------------------------------------------------------------- | | The first thing we will do is create a new Laravel application instance | which serves as the "glue" for all the components of Laravel, and is | the IoC container for the system binding all of the various parts. | */ $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); /* |-------------------------------------------------------------------------- | Bind Important Interfaces |-------------------------------------------------------------------------- | | Next, we need to bind some important interfaces into the container so | we will be able to resolve them when needed. The kernels serve the | incoming requests to this application from both the web and CLI. | */ $app->singleton( 'Illuminate\Contracts\Http\Kernel', 'App\Http\Kernel' ); $app->singleton( 'Illuminate\Contracts\Console\Kernel', 'App\Console\Kernel' ); $app->singleton( 'Illuminate\Contracts\Debug\ExceptionHandler', 'App\Exceptions\Handler' ); /* |-------------------------------------------------------------------------- | Return The Application |-------------------------------------------------------------------------- | | This script returns the application instance. The instance is given to | the calling script so we can separate the building of the instances | from the actual running of the application and sending responses. | */ return $app;
同样,第15
行的__DIR.'/../'
指的是项目的根目录,所以我们改成:
$app = new Illuminate\Foundation\Application( realpath(APP_DIR) );
最后,我们将public/index.php
里的引用代码改成为laravel/bootstrap/
下的这两个文件就可以了,我们定义一个常量LARAVEL_DIR
指向laravel/
文件夹以便我们写路径。
对了,差点忘记还得在public/index.php
开头加上require __DIR__.'/../vendor/autoload.php'
。
到这里,我们的项目就可以正常运行了。
等一下!是不是漏了什么
对了,还有最开始看的artisan
文件。我们再打开看看:
#!/usr/bin/env php <?php /* |-------------------------------------------------------------------------- | Register The Auto Loader |-------------------------------------------------------------------------- | | Composer provides a convenient, automatically generated class loader | for our application. We just need to utilize it! We'll require it | into the script here so that we do not have to worry about the | loading of any our classes "manually". Feels great to relax. | */ require __DIR__.'/bootstrap/autoload.php'; $app = require_once __DIR__.'/bootstrap/app.php'; /* |-------------------------------------------------------------------------- | Run The Artisan Application |-------------------------------------------------------------------------- | | When we run the console application, the current CLI command will be | executed in this console and the response sent back to a terminal | or another output device for the developers. Here goes nothing! | */ $kernel = $app->make('Illuminate\Contracts\Console\Kernel'); $status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput ); /* |-------------------------------------------------------------------------- | Shutdown The Application |-------------------------------------------------------------------------- | | Once Artisan has finished running. We will fire off the shutdown events | so that any final work may be done by the application before we shut | down the process. This is the last thing to happen to the request. | */ $kernel->terminate($input, $status); exit($status);
这里也引用了bootstrap/
,但是我们已经把它移到laravel/
了,我们就修改一下吧。
但是这里改一下那里改一下,也太乱了吧……
那么优化一下。
我们先不动artisan
并还原application/public/index.php
,然后在项目目录下也添加一个bootstrap/
目录,添加bootstrap/autoload.php
、bootstrap/app.php
两个文件,文件内容很简单,直接引用laravel/bootstrap/
下对应的两个文件,并把require __DIR__.'/../vendor/autoload.php'
放在这里的autoload.php
中。当然我们还是要定义LARAVEL_DIR
和APP_DIR
,我们在项目根目录下新建一个path.php
文件,把路径定义放进去,然后在public/index.php
和artisan
里加上对path.php
的引用就大功告成了。
对原始文件的改动少多了,你的代码洁癖症
有没有感觉好一些?
就这样完了?这样肯定有问题!
目前我确实发现了一个问题。
php artisan optimize
命令
在执行php artisan optimize
命令的时候会出现错误:
[InvalidArgumentException] Configuration file "/application_1/,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,/application_1/app/Providers/App ServiceProvider.php,/application_1/app/Providers/BusServiceProvider.php,/application_1/app/Providers/ConfigSer viceProvider.php,/application_1/app/Providers/EventServiceProvider.php,/application_1/app/Providers/RouteServi ceProvider.php" does not exist.
这是因为optimize
命令里写死了框架核心文件的路径,必须从$app['path.base'].'vendor/'
里加载,而$app['path.base']
则是指向的项目根目录(我觉得这样写死很不科学,不过这是传说中的规范。Laravel
官方并不推荐将框架分离出去,所以基于这个出发点,写死做法也没有问题)。
我们可以单独实现一个optimize
命令来解决这个问题(我的就叫做optimize-separated
),或者我们在laravel/
目录下执行composer dumpautoload -o
,也能获得差不多的性能优化。
优化
程序可以正常运行了,不过我们还得优化一下结构,
Laravel
是一个更新非常频繁、社区非常活跃的框架,这意味着版本更新会很快。版本升级通常会有一些目录结构的改变(3到4,4到5,变化都很大),有些是推荐性的、有些是强制性的,所以一年后我们的laravel/
可能在用Laravel 6
了,项目文件结构发生了很大的改变,而我们并不想去修改一年前项目的结构,假设我们使用的是LTS
(长期支持)版本,我们也不需要紧跟最新的大版本。所以我们做一个简单的修改。
application/ laravel/ laravel5.0/
把laravel/
的文件移到laravel5.0/
即可,以后升级了我们就再开一个目录,例如laravel5.1/
。
最后将laravel/laravel5.0/vendor/
文件夹从laravel/laravel5.0/.gitignore
中移除,提交到版本控制服务器,团队中其他人只需拉取你提交的框架而不用执行composer install
了。框架代码最好由一个人维护,以免造成代码冲突。
公用库
篇幅有限,这里只讲一下解决方案。我们主要利用命名空间
和Composer
来实现。
首先需要修改laravel/laravel5.0/composer.json
文件的这一段:
... "autoload-dev": { "classmap": [ ] }, ...
我们添加一个配置:
... "autoload": { "classmap": [ ], "psr-4": { "Common\\": "../common/" } }, ...
这里主要使用了PSR-4
规范(和application/
的composer.json
一样。具体规范这里就不细说了)。
那么我们就可以开始写公用库了!新建文件laravel/common/Add.php
,输入以下内容:
<?php namespace Common; class Add { static function execute($a, $b) { return $a + $b; } }
然后在laravel/laravel5.0/
目录下执行命令生成新的自动加载配置:
composer dumpautoload
接下来我们就可以直接使用\\Common\\Add::execute(1, 2)
了。我这里将common/
文件夹放在了laravel/
文件夹中,如果你的代码库和Laravel
某个版本有依赖关系,那么放在指定版本的Laravel
文件夹中更科学。
因为包含了一点点推理,也许本文会让人觉得有点复杂,你可以参考文章开头的git
仓库,里面仅包含直接有效的操作步骤和说明。