前言
终于有那么点时间能将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
仓库,里面仅包含直接有效的操作步骤和说明。
这篇文章就这么完了。