欢迎光临
我们一直在努力

多项目共用Laravel框架示例

前言

终于有那么点时间能将Laravel 5的一些好的实践总结出来,希望为普及Laravel和新的PHP编程思想出一份力。如有错误或你有更好的方式,请不吝赐教,共同进步。

本文有配套的git仓库,你也可以clone我的代码仓库,里面包含每一步的操作(没有多余步骤)。

git clone git@git.oschina.net:notfound/Separated-Laravel.git

正文

通常很多项目都会依赖同一个的框架,还会共用很多代码库,手动复制粘贴这些文件到每个项目文件夹显然很伤害键盘,特别当项目多了之后,手动管理各种不同版本的库极容易精神分裂。为避免让搬砖这项工作对身体、精神造成双重伤害,最好将这些文件公用化。那么有哪些方法呢?

使用Composer

Composer是什么?ComposerPHP库的管理工具。简单来说就是所有库都要告诉Composer自己依赖哪些库,这样当你告诉Composer你需要哪些库(甚至特定的版本)的时候,Composer就可以把你指定的库以及他们的依赖帮你全部下载到项目中。

很多人都用过老版本的ThinkPHP或者CodeIgniter,那么一定对importvendor$this->load这些函数记忆犹新,他们经常在构造方法里成群出现,形成一道靓丽的风景线。那些日子可以忘掉了。使用Composer,只要遵循PSR-0PSR-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框架。我知道有些人表示很担心,所以首先确定几个基本原则

  1. 不改变Laravel项目的目录结构、不要改动框架代码,方便未来升级;
  2. 不用奇怪的hack方式实现(通用性不强);
  3. 不会给新建项目带来一些配置麻烦(比如得通过ln -s映射一些目录);
  4. 没有任何功能遗失(我们想感受Laravel所有的优点)。

OK,明确了基本原则,我们来看看设想的、分离之后的目录结构:

application/
laravel/

 

application是项目目录,laravelLaravel框架的目录,清晰明了。

我们再来看看一个官方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

 

如果你在使用ThinkPHPCodeIgniter等没有采用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

artisanLaravel的特色之一,如果想要在项目目录执行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.jsonrequire部分明明是这样:

...
    "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.ymlphpunit.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.phpbootstrap/

大块头都移走了,我们再从入口文件开始看:

<?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.php30行改为:

$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.phpbootstrap/app.php两个文件,文件内容很简单,直接引用laravel/bootstrap/下对应的两个文件,并把require __DIR__.'/../vendor/autoload.php'放在这里的autoload.php中。当然我们还是要定义LARAVEL_DIRAPP_DIR,我们在项目根目录下新建一个path.php文件,把路径定义放进去,然后在public/index.phpartisan里加上对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仓库,里面仅包含直接有效的操作步骤和说明。

赞(0)
版权归原作者所有,如有侵权请告知。达维营-前端网 » 多项目共用Laravel框架示例

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址