1. 认识 Phalcon

Phalcon是一个C拓展方式的PHP框架,安装拓展成功即可使用,性能很高。

Phalcon是一个高度解耦的框架。就像给了你一堆积木,你是拼成变形金刚呢还是芭比娃娃呢,随意打造。但是也提高了门槛,小白面对空文件夹不知道如何上手,因此官方有了phalcon-devtools工具帮你快速搭建框架;也有了包含了许多注册好的服务默认的工厂类FactoryDefault。

这是一个提供了 MVC 模式的框架,上手需要了解一下Phalcon中使用模型,使用控制器,和使用视图的一些操作。

Phalcon算是比较现代化的框架了,所以在使用之前先把命名空间,DI等等了解了。

全栈框架,该有的都有了;但是不顺手的地方也可以自己打造,非常自由,简直放飞自我。

本文的目的就是对Phalcon的不同项目中的项目结构捋一遍,从而对Phalcon框架的框架架构有一个初步的认识;然而在实际开发中,我们的代码架构会更加复杂一些。

2. 开始入手

2.1 安装

  • 先安装Phalcon;
  • 然后安装phalcon-devtools;我们通过phalcon-devtools构建一些项目demo来了解这个框架。刚上手时,phalcon-devtools这个工具特别有用。

2.2 使用 phalcon-devtools

学会使用phalcon-devtools在一开始学习的时候特别有用。
在自动化创建一个项目时,phalcon-devtools工具提供了 cli, micro, simple, modules 四种模式的项目结构。将这四个看一遍就可以对整体架构有一定的认识。

2.3 关于MVC架构

Github上有官方提供的MVC架构的例子:https://github.com/phalcon/mvc

3. 微应用(micro类型项目)

3.1 工具创建一个微应用

命令行中执行以下语句创建一个micro类型的项目:
phalcon create-project Phalcon-micro micro
可以看到大概是这么个目录:

Phalcon-micro/
    .phalcon/
    app/
        config/
        migrations/
        models/
        views/
        app.php
    public/
        css/
        files/
        img/
        js/
        temp/
        .htaccess
        index.php
    .htaccess
    index.html

微框架应用只需要书写极少的代码即可创建一个PHP应用,适用于书写小的应用, API或原型等。

3.2 入口文件index.php

public/index.php文件内容解析:

<?php

use Phalcon\Di\FactoryDefault; // 工厂默认DI
use Phalcon\Mvc\Micro; // 引入微应用

error_reporting(E_ALL); // 设置报错级别

define('BASE_PATH', dirname(__DIR__)); // 定义根目录
define('APP_PATH', BASE_PATH . '/app'); // 定义app目录

try {

    /**
     * FactoryDefault已经自动注册了提供全栈框架功能的服务。这些默认服务可以根据自己的习惯重写。
     */
    $di = new FactoryDefault(); // 实例化FactoryDefault

    /**
     * 包含service文件,即是用来注册服务的文件
     */
    include APP_PATH . '/config/services.php'; // 引入app/config/service.php

    /**
     * 容器中获取配置服务
     */
    $config = $di->getConfig();

    /**
     * 包含自动加载文件
     */
    include APP_PATH . '/config/loader.php'; // 引入app/config/loader.php

    /**
     * 实例化微应用,并传入容器示例
     */
    $app = new Micro($di);

    /**
     * 包含应用引导文件
     */
    include APP_PATH . '/app.php'; // 引入app/app.php

    /**
     * 执行控制请求
     */
    $app->handle();

} catch (\Exception $e) {
      echo $e->getMessage() . '<br>';
      echo '<pre>' . $e->getTraceAsString() . '</pre>';
}

接下来根据入口文件的流程,来走一遍,config.php文件就不说了

3.3 服务注册文件service.php

<?php

use Phalcon\Mvc\View\Simple as View;
use Phalcon\Mvc\Url as UrlResolver;

/**
 * 单例模式(共享)的配置服务
 */
$di->setShared('config', function () {
    return include APP_PATH . "/config/config.php";
});

/**
 * 设置视图组件(全局共享)
 */
$di->setShared('view', function () {
    $config = $this->getConfig(); // 获取配置

    $view = new View(); // 实例化视图
    $view->setViewsDir($config->application->viewsDir); // 设置视图路径(从配置文件config.php中获取)
    return $view; // 返回实例
});

/**
 * URL组件,用来生成应用所有类型的url
 */
$di->setShared('url', function () {
    $config = $this->getConfig(); // 获取配置

    $url = new UrlResolver(); // 实例化url类
    $url->setBaseUri($config->application->baseUri); // 设置基URI(从配置文件config.php中获取)
    return $url; // 返回url类示例
});

/**
 * 数据库连接,根据配置文件中的设置
 */
$di->setShared('db', function () {
    $config = $this->getConfig(); // 获取配置

    $class = 'Phalcon\Db\Adapter\Pdo\\' . $config->database->adapter; // 数据库类型(Mysql),根据类型定位到对应的数据库操作PDO类
    $params = [
        'host'     => $config->database->host,
        'username' => $config->database->username,
        'password' => $config->database->password,
        'dbname'   => $config->database->dbname,
        'charset'  => $config->database->charset
    ]; // 读取其它配置

    /* 如果数据库是Postgresql,注销掉charset参数 */
    if ($config->database->adapter == 'Postgresql') {
        unset($params['charset']);
    }

    $connection = new $class($params); // 实例化数据库实例

    return $connection; // 返回数据库实例
});

3.4 自动加载配置文件loader.php

<?php

/**
 * 注册自动加载
 */
$loader = new \Phalcon\Loader();

/* 这里自动加载了modelsDir路径(配置) */
$loader->registerDirs(
    [
        $config->application->modelsDir
    ]
)->register();

3.5 应用引导文件app.php

<?php
/**
 * Local variables 本地变量
 * @var \Phalcon\Mvc\Micro $app 微应用实例
 */

/**
 * 在这里添加路由
 * 根目录默认index
 */
$app->get('/', function () {
    echo $this['view']->render('index');
});

/**
 * 找不到时的操作(返回404)
 */
$app->notFound(function () use($app) {
    $app->response->setStatusCode(404, "Not Found")->sendHeaders(); // 设置状态码404
    echo $app['view']->render('404'); // 转到404页面
});

这样基本上这个微应用,Phalcon最简单的一种实现就跑起来了。

3.6 小结

一个内置的微应用,可以快速构建简单的应用。

4. 简单的单模块应用(simple类型项目)

4.1 大概的项目目录

Phalcon-simple/
    app/
        config/
            config.php
            loader.php
            router.php
            services.php
        controllers/
            ControllerBase.php
            IndexController.php
        library/
        migrations/
        models/
        views/
            index/
                idnex.volt
            index.volt
    cache/
    public/
        css/
        files/
        img/
        js/
        temp/
        .htaccess
        index.php
    .htaccess
    .htrouter.php
    index.html

4.2 index.php

同样从index.php入口文件入手:

<?php
use Phalcon\Di\FactoryDefault; // 工厂默认DI

error_reporting(E_ALL);

define('BASE_PATH', dirname(__DIR__)); // 定义入口根目录路径
define('APP_PATH', BASE_PATH . '/app'); // 定义app目录路径

try {

    /**
     * 实例化默认工厂DI,已经自动注册了很多提供全栈框架的服务
     */
    $di = new FactoryDefault();

    /**
     * 引入router路由文件
     */
    include APP_PATH . '/config/router.php';

    /**
     * 引入services服务注册文件
     */
    include APP_PATH . '/config/services.php';

    /**
     * 为下面的内置设置,获取配置服务
     */
    $config = $di->getConfig();

    /**
     * 包含自动加载loader.php文件
     */
    include APP_PATH . '/config/loader.php';

    /**
     * 加载请求控制
     */
    $application = new \Phalcon\Mvc\Application($di);

    echo str_replace(["\n","\r","\t"], '', $application->handle()->getContent()); // 移除换行,回车,跳格

} catch (\Exception $e) {
    echo $e->getMessage() . '<br>';
    echo '<pre>' . $e->getTraceAsString() . '</pre>';
}

4.3 router.php路由文件

获取路由服务,定义路由,执行。

<?php

$router = $di->getRouter();

// Define your routes here
// 在这里定义路由

$router->handle();

4.4 services.php服务注册文件

<?php

use Phalcon\Mvc\View; // view层
use Phalcon\Mvc\View\Engine\Php as PhpEngine; // view层php引擎
use Phalcon\Mvc\Url as UrlResolver; // url组件
use Phalcon\Mvc\View\Engine\Volt as VoltEngine; // volt引擎
use Phalcon\Mvc\Model\Metadata\Memory as MetaDataAdapter; // 元数据适配器
use Phalcon\Session\Adapter\Files as SessionAdapter; // session文件适配器
use Phalcon\Flash\Direct as Flash; // 闪存服务

/**
 * 共享的配置服务
 */
$di->setShared('config', function () {
    return include APP_PATH . "/config/config.php"; // 配置文件所在路径
});

/**
 * URL组件,用来生成所有类型的应用url
 */
$di->setShared('url', function () {
    $config = $this->getConfig(); // 获取配置

    $url = new UrlResolver(); // 实例化
    $url->setBaseUri($config->application->baseUri); // 传入基URI

    return $url; // 返回URL实例
});

/**
 * 设置视图组件
 */
$di->setShared('view', function () {
    $config = $this->getConfig(); // 获取配置

    $view = new View(); // 实例化
    $view->setDI($this); // 设置为服务
    $view->setViewsDir($config->application->viewsDir); // 传入views路径

    // 注册模板引擎; 注册了.volt和.phtml结尾的文件
    $view->registerEngines([
        '.volt' => function ($view) {
            $config = $this->getConfig(); // 获取配置

            $volt = new VoltEngine($view, $this); // 实例化引擎

            $volt->setOptions([
                'compiledPath' => $config->application->cacheDir,
                'compiledSeparator' => '_'
            ]); // 设置参数;缓存路径和缓存文件连接符

            return $volt; // 返回实例
        },
        '.phtml' => PhpEngine::class

    ]);

    return $view; // 返回实例
});

/**
 * 数据库连接
 */
$di->setShared('db', function () {
    $config = $this->getConfig(); // 获取配置

    $class = 'Phalcon\Db\Adapter\Pdo\\' . $config->database->adapter; //数据库类型(mysql)
    $params = [
        'host'     => $config->database->host,
        'username' => $config->database->username,
        'password' => $config->database->password,
        'dbname'   => $config->database->dbname,
        'charset'  => $config->database->charset
    ];

    /* 如果是Postgresql数据库,删除charset参数 */
    if ($config->database->adapter == 'Postgresql') {
        unset($params['charset']);
    }

    $connection = new $class($params); //实例化

    return $connection; //返回连接实例
});


/**
 * 如果配置了元数据适配器就使用原数据适配器否则使用内存
 */
$di->setShared('modelsMetadata', function () {
    return new MetaDataAdapter();
});

/**
 * 注册Bootstrap框架类中的会话闪存服务
 */
$di->set('flash', function () {
    return new Flash([
        'error'   => 'alert alert-danger',
        'success' => 'alert alert-success',
        'notice'  => 'alert alert-info',
        'warning' => 'alert alert-warning'
    ]);
});

/**
 * 启动session,当有组件请求session服务的时候
 */
$di->setShared('session', function () {
    $session = new SessionAdapter(); // session适配器
    $session->start();

    return $session;
});

可以看到simple项目已经使用了view层,引入了volt引擎,session服务等,甚至还注册前端bootstrap框架的class。

4.5 loader.php自动加载文件

<?php

$loader = new \Phalcon\Loader();

/**
 * 自动加载配置文件中定义好的路径
 */
$loader->registerDirs(
    [
        $config->application->controllersDir,
        $config->application->modelsDir
    ]
)->register();

4.6 controllers

controllers下定义了一个ControllerBase类,和一个IndexController类:
ControllerBase.php

<?php

use Phalcon\Mvc\Controller;

class ControllerBase extends Controller
{

}

IndexController.php

<?php

class IndexController extends ControllerBase
{

    public function indexAction()
    {

    }

}

基本上simple这个项目就是一个简单的省略了modle的基本单模块项目。

4.7 小结

一个简单的单模块web应用。

5. cli命令行应用(cli类型项目)

5.1 大概的项目目录

Phalcon-cli/
    app/
        config/
            config.php
            loader.php
            services.php
        models/
        tasks/
            MainTask.php
            VersionTask.php
        bootstrap.php
    public/
        css/
        js/
    run

cli项目的架构看上去更加简单明了。

5.2 run文件

run文件是用来执行项目的入口文件,只需要在命令行执行:

php run

输出:

Congratulations! You are now flying with Phalcon CLI!

这是在MainTask.php中输出的内容。
看一下run文件,就是执行了一段php代码,而这段代码只是引入的bootstrap引导文件:

#!/usr/bin/env php
<?php
include __DIR__ . "/app/bootstrap.php";

5.3 bootstrap.php引导程序

<?php

use Phalcon\Di\FactoryDefault\Cli as CliDi; //引入默认工厂DI的cli模式
use Phalcon\Cli\Console as ConsoleApp; // 引入用来创建cli应用的Console组件

define('BASE_PATH', dirname(__DIR__));
define('APP_PATH', BASE_PATH . '/app');

/**
 * 实例化默认工厂DI的cli模式,自动加载提供了全栈框架的服务
 */
$di = new CliDi();

/**
 * 包含服务注册文件
 */
include APP_PATH . '/config/services.php';

/**
 * 获取配置服务中的配置
 */
$config = $di->getConfig();

/**
 * 包含自动加载
 */
include APP_PATH . '/config/loader.php';

/**
 * 创建一个console应用(cli)
 */
$console = new ConsoleApp($di);

/**
 * 处理console参数
 */
$arguments = [];

foreach ($argv as $k => $arg) {
    if ($k == 1) {
        $arguments['task'] = $arg; // 第一个参数task类
    } elseif ($k == 2) {
        $arguments['action'] = $arg; // 第二个参数action方法
    } elseif ($k >= 3) {
        $arguments['params'][] = $arg; // 第三个参数,额外参数(数组)
    }
}

try {

    /**
     * Handle传入处理好的参数
     */
    $console->handle($arguments);

    /**
     * 如果配置中printNewLine设置了true,将会在末尾另起一行
     *
     * 仅仅只是为了在命令行中更直观的表现
     */
    if (isset($config["printNewLine"]) && $config["printNewLine"]) {
        echo PHP_EOL; // 输出一个换行符
    }

} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
    echo $e->getTraceAsString() . PHP_EOL;
    exit(255);
}

5.4 service.php服务注册

注册了配置服务和数据库服务:

<?php

/**
 * 注册共享配置服务
 */
$di->setShared('config', function () {
    return include APP_PATH . '/config/config.php'; // 配置文件路径
});

/**
 * 数据库连接
 */
$di->setShared('db', function () {
    $config = $this->getConfig(); // 获取配置

    $class = 'Phalcon\Db\Adapter\Pdo\\' . $config->database->adapter; // 数据库类型(Mysql)

    $params = [
        'host'     => $config->database->host,
        'username' => $config->database->username,
        'password' => $config->database->password,
        'dbname'   => $config->database->dbname,
        'charset'  => $config->database->charset
    ];

    /* 如果数据库是Postgresql,删除charset变量 */
    if ($config->database->adapter == 'Postgresql') {
        unset($params['charset']);
    }

    $connection = new $class($params); // 数据库实例

    return $connection; // 返回数据库实例
});

5.5 loader.php自动加载

cli项目与普通web项目不一样的是,自动加载了tasks目录和models目录,但实际上道理是一样的,task任务就好比是Controller。

<?php
$loader = new \Phalcon\Loader();
$loader->registerDirs([
    APP_PATH . '/tasks',
    APP_PATH . '/models'
]);
$loader->register();

5.6 tasks目录

看一下这个demo项目的两个task文件。
MainTask.php:

<?php

class MainTask extends \Phalcon\Cli\Task
{
    public function mainAction()
    {
        echo "Congratulations! You are now flying with Phalcon CLI!";
    }
}

VersionTask.php:

<?php

class VersionTask extends \Phalcon\Cli\Task
{
    public function mainAction()
    {
        $config = $this->getDI()->get('config'); //获取配置

        echo $config['version']; //输出版本
    }
}

都继承了 Phalcon\Cli\Task 类;MainTask 为系统默认类,类似于默认IndexController那么个意思。

5.7 小结

一个cli模式的demo,可以用来编写简单的cli脚本。

6. 多模块项目(modules类型项目)

6.1 项目目录

这个多模块项目就文件稍微多一点了,可以看到这里有两个模块,一个是web项目,一个是cli项目。

Phalcon-modules
    app/
        common/
            library/
            models/
        config/
            config.php
            loader.php
            routes.php
            services.php
            services_cli.php
            services_web.php
        modules/
            cli/
                migrations/
                tasks/
                    MainTask.php
                    VersionTask.php
                Module.php
            frontend/
                controllers/
                    ControllerBase.php
                    IndexController.php
                models/
                views/
                    index/
                        index.volt
                    index.volt
                Module.php
        bootstrap_cli.php
        bootstrap_web.php
    cache/
        volt/
    public/
        css/
        files/
        img/
        js/
        temp/
        .htaccess
        index.php
    .htaccess
    .htrouter.php
    index.html
    run

大概地看一下文件,可以发现这个模块是simple项目和cli项目合并起来的一个项目,所以之前看过的内容我们就不重复关注了,我们还是重点放在整体架构分析和不同的地方。

6.2 index.php入口文件

依然是入口文件,不过这次这个入口文件变了:

<?php
require '../app/bootstrap_web.php';

只有这么一句,因为public入口下指引的应该是web项目,所以这里直接引入了web项目的指引程序文件bootstrapweb.php。
基本相当于将各个模块的入口文件放到了app目录下,以bootstrap
开头命名:

common/
config/
modules/
    cli/
        migrations/
        tasks/
        Module.php
    frontend/
        controllers/
        models/
        views/
        Module.php
bootstrap_cli.php
bootstrap_web.php

common为公共的模块,公共类公共模型什么的放这里;config配置目录,与之前的区别是,services.php放公共服务文件,services_cli.php放cli模块的服务文件,services_web.php放web项模块的服务文件。modules则为两个模块的具体文件,区别是底下多了一个Module.php。

6.3 config.php配置文件

配置文件中的路径配置不再配置到Controller,model层面,只配置到外一层和公共模块下。

'application' => [
        'appDir'         => APP_PATH . '/',
        'modelsDir'      => APP_PATH . '/common/models/',
        'migrationsDir'  => APP_PATH . '/migrations/',
        'cacheDir'       => BASE_PATH . '/cache/',
        'baseUri'        => preg_replace('/public([\/\\\\])index.php$/', '', $_SERVER["PHP_SELF"]),
    ]

6.4 routes.php路由文件

配置路由

<?php

$router = $di->getRouter();

foreach ($application->getModules() as $key => $module) {
    $namespace = preg_replace('/Module$/', 'Controllers', $module["className"]);
    $router->add('/'.$key.'/:params', [
        'namespace' => $namespace,
        'module' => $key,
        'controller' => 'index',
        'action' => 'index',
        'params' => 1
    ])->setName($key);
    $router->add('/'.$key.'/:controller/:params', [
        'namespace' => $namespace,
        'module' => $key,
        'controller' => 1,
        'action' => 'index',
        'params' => 2
    ]);
    $router->add('/'.$key.'/:controller/:action/:params', [
        'namespace' => $namespace,
        'module' => $key,
        'controller' => 1,
        'action' => 2,
        'params' => 3
    ]);
}

6.5 loader.php自动加载文件

<?php

use Phalcon\Loader;

$loader = new Loader();

/**
 * 注册命名空间;common下的公共模型,库等
 */
$loader->registerNamespaces([
    'PhalconModules\Models' => APP_PATH . '/common/models/',
    'PhalconModules'        => APP_PATH . '/common/library/',
]);

/**
 * 注册模块下的类
 */
$loader->registerClasses([
    'PhalconModules\Modules\Frontend\Module' => APP_PATH . '/modules/frontend/Module.php', // web模块的Module.php
    'PhalconModules\Modules\Cli\Module'      => APP_PATH . '/modules/cli/Module.php' // cli模块的Module.php
]);

$loader->register();

6.6 Module.php模块文件

在单模块项目中的路径加载配置,服务加载配置等需要区别开不同模块的,都移到了Module.php文件,以实现 ModuleDefinitionInterface 模块定义接口的形式实现。
比如Web模块的Module.php文件:

<?php
namespace PhalconModules\Modules\Frontend;

use Phalcon\DiInterface;
use Phalcon\Loader;
use Phalcon\Mvc\View;
use Phalcon\Mvc\View\Engine\Php as PhpEngine;
use Phalcon\Mvc\ModuleDefinitionInterface;

class Module implements ModuleDefinitionInterface
{
    /**
     * 注册模块的自动加载
     *
     * @param DiInterface $di
     */
    public function registerAutoloaders(DiInterface $di = null)
    {
        $loader = new Loader();

        /* 在这里定义当前模块下的Controllers,Models等目录路径 */
        $loader->registerNamespaces([
            'PhalconModules\Modules\Frontend\Controllers' => __DIR__ . '/controllers/',
            'PhalconModules\Modules\Frontend\Models' => __DIR__ . '/models/',
        ]);

        $loader->register();
    }

    /**
     * 注册模块的服务
     *
     * @param DiInterface $di
     */
    public function registerServices(DiInterface $di)
    {
        /**
         * 设置视图组件
         */
        $di->set('view', function () {
            $view = new View();
            $view->setDI($this);
            $view->setViewsDir(__DIR__ . '/views/'); // 视图路径

            $view->registerEngines([
                '.volt'  => 'voltShared',
                '.phtml' => PhpEngine::class
            ]); // 注册视图引擎

            return $view;
        });
    }
}

cli模块的Module.php:

<?php
namespace PhalconModules\Modules\Cli;

use Phalcon\DiInterface;
use Phalcon\Loader;
use Phalcon\Mvc\ModuleDefinitionInterface;

class Module implements ModuleDefinitionInterface
{
    /**
     * 注册模块的自动加载
     *
     * @param DiInterface $di
     */
    public function registerAutoloaders(DiInterface $di = null)
    {
        $loader = new Loader();

        $loader->registerNamespaces([
            'PhalconModules\Modules\Cli\Tasks' => __DIR__ . '/tasks/',
        ]);

        $loader->register();
    }

    /**
     * 注册模块的服务
     *
     * @param DiInterface $di
     */
    public function registerServices(DiInterface $di)
    {
    }
}

根据这样的一个套路,我们可以引入更多的模块。

6.7 小结

多模块的设计思路,按照这个套路,我们可以自由地搭建起我们的框架并应用到项目中。