2016 年如何优雅使用 CodeIgniter 3?

HexHex

作者:木由子 (微信: MI_rifle)
原文:知加 - 书中自有黄金屋

CodeIgniter 无疑是一个设计精巧的框架,但随着技术的日新月异,由于过于久远,有些地方现在用起来变得不是特别舒服。

比如说:

  1. php 版本的选择
  2. 开发环境的搭建
  3. 为了单一入口的 defined('BASEPATH') OR exit('')
  4. composer 包的引入
  5. 基于 namespace 组织代码
  6. 控制器之间的相互调用
  7. 加载之前必须先调用 $this->load->xxx
  8. 用户前台、管理后台、api 接口的分组

接下来晒一下我的解决方案,先一起看下重组后的目录

app                        // App 命名空间根目录
  Models                   // App\Models 用来存放业务模型
  Services                 // App\Services 用来存放系统组件
config                     // CI 自带配置目录
  app.php                  // 程序实际入口
controllers                // 控制器目录
  admin                    // 分组 - 后台管理
  api                      // 分组 - api 接口
  develop                  // 
  user                     // 分组 - 用户前台
public                     // 网站对外根目录
  index.php                // 网站单一入口
resources                  // 资源目录
  assets                   // css,img,js
  helpers                  // 帮助函数
    functions.php          //
  language                 // 多国语目录
  libraries                // 第三方野生类库
  views                    // 视图目录
scripts                    // 脚本目录
  serve.php                //
storage                    // 读写目录
  cache                    // 文件缓存
  logs                     // 文件日志
vendor                     // composer 类库加载目录
composer.json

首先从 1. php 的版本选择说起,虽然 ci 能很好的运行在 5.3 甚至 5.2 上,但在 7.1 已经发布的当下,即使最低最低还是推荐 5.4+。

顺带着 2. 开发环境的搭载 就可以用 php 5.4+ 自带的命令来运行

php -S localhost:9000 -t public scripts/serve.php

在 serve.php 当中我们进行了隐藏 index.php 的操作,以下是 scripts/serve.php 的内容:

<?php

// 应用根目录
$root = dirname(__DIR__);

// 获取访问路径
$_SERVER['PATH_INFO'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// 转义字符
$_SERVER['PATH_INFO'] = urldecode($_SERVER['PATH_INFO']);

// 排除根目录与 index.php 开头
if (in_array(substr($_SERVER['PATH_INFO'], 0, 10), ['/', '/index.php'])) ;
else {

    // 检查文件
    if (file_exists($root . '/public' . $_SERVER['PATH_INFO'])) {
        return false;
    }

    // 设置脚本
    $_SERVER['SCRIPT_NAME'] = 'index.php';
}

// 执行脚本
require_once $root . '/public/index.php';

由于将用户访问根目录以 public 来进行了隔离,问题 3 也得到了完美的解决。

以下是 public/index.php 的内容:

<?php

// 请求程序入口
require_once dirname(__DIR__) . '/config/app.php';

问题 4 和 5 其实可以放到一起来说,CodeIgniter 本身其实是有 composer 包的,官方包名为 codeigniter/framework 点击访问,既然如此咱么可以用 composer 的方式来引入 CodeIgniter 到我们的项目,以下是 composer.json 的内容:

{
  "name": "sinoci/sinoci",
  "homepage": "https://github.com/sinoci/sinoci",
  "license": "MIT",
  "require": {
    "php": ">=5.4.0",
    "codeigniter/framework": "3.1.*"
  },
  "autoload": {
    "files": [
      "resources/helpers/functions.php"
    ],
    "psr-4": {
      "App\\": "app/"
    }
  }
}

这里定义了 App 命名空间,解决了 5. 基于 namespace 组织代码

安装完可以发现 CI 的实际运行入口是 vendor/codeigniter/framework/system/core/CodeIgniter.php,也就是说咱们的实际运行流程是:

> public/index.php
> config/app.php
> vendor/codeigniter/framework/system/core/CodeIgniter.php

这样 config/app.php 该做的事也就一目了然了,以下是 config/app.php 的内容:

<?php

// 设置环境变量
$_['APP_ENV'] = getenv('APP_ENV') ?: 'develop';

// 是否调试模式
$_['APP_DEBUG'] = (getenv('APP_DEBUG') OR $_['APP_ENV'] === 'develop');

// 设置应用目录
$_['APPPATH'] = dirname(__DIR__) . '/';

// 设置 CI 框架目录
$_['BASEPATH'] = $_['APPPATH'] . 'vendor/codeigniter/framework/system/';

// 设置模版目录
$_['VIEWPATH'] = $_['APPPATH'] . 'resources/views/';

// 循环定义系统常量
array_walk($_, function ($v, $k) {
    defined($k) OR define($k, $v);
});

// 设置运行环境与控制器分组
define('ENVIRONMENT', $routing['directory'] = APP_ENV);

// 调试环境下配置
if (APP_DEBUG) {
    ini_set('display_errors', 1);
    ini_set('error_reporting', -1);
    ini_set('opcache.enable', 0);
}

// 请求 CI 框架入口
require_once BASEPATH . 'core/CodeIgniter.php';

终于完事了么?这样还不算完,CI 在运行的过程中必须的文件

  • config/config.php
  • config/constants.php
  • config/routes.php

也是要补上的,以下是各自的内容:

config/config.php

<?php

// 引入 初始配置
require_once dirname(BASEPATH) . '/application/config/config.php';

// 配置 缓存目录
$config['cache_path'] = APPPATH . 'storage/cache';

// 开启 composer 自动加载
$config['composer_autoload'] = true;

// 开启 钩子系统
$config['enable_hooks'] = true;

// 设置私有密钥
$config['encryption_key'] = '^encryption_key$';

// 配置 错误提示模版目录
$config['error_views_path'] = dirname(BASEPATH) . '/application/views/errors/';

// 配置 默认语言
$config['language']    = 'chinese';

// 配置 日志目录
$config['log_path'] = APPPATH . 'storage/logs';

// 配置 会话目录
$config['sess_save_path'] = APPPATH . 'storage/sessions';

config/constants.php

<?php

// 引入 初始常量配置
require_once dirname(BASEPATH) . '/application/config/constants.php';

config/routes.php

<?php

// 引入 初始路由配置
require_once dirname(BASEPATH) . '/application/config/routes.php';

至此就可以算是初步完成了。为什么是初步呢?

因为 CI 代码久远的问题,在我们刚弄热乎的新结构的运行过中,框架本身会抛出一些错误异常。那如何屏蔽呢?这里就需要 CI 的钩子机制

修改 config/constants.php

<?php

use App\Services\Event;

// 引入 初始常量配置
require_once dirname(BASEPATH) . '/application/config/constants.php';

// 捕获异常退出
register_shutdown_function(function () {
    error_get_last() && call_user_func_array([new Event, 'error'], error_get_last());
});

添加 config/hooks.php

<?php

use App\Services\Event;

$hook['post_controller_constructor'][] = function () {
    // 重新绑定实例
    $app =& get_instance();
    $app = app();
};

$hook['pre_system'][] = function () {
    // 绑定异常处理
    set_error_handler([new Event, 'error']);
    set_exception_handler([new Event, 'exception']);
};

添加 app/Services/Event.php

<?php

namespace App\Services;

/**
 * 框架组件 - 事件
 *
 * @package App\Services
 */
class Event
{

    /**
     * 触发错误处理
     *
     * @return void
     */
    public function error()
    {
        // 捕捉非框架错误
        str_contains(func_get_arg(2), str_replace('/', DIRECTORY_SEPARATOR, BASEPATH)) OR call_user_func_array('_error_handler', func_get_args());
    }

    /**
     * 触发异常处理
     *
     * @return void
     */
    public function exception()
    {
        // 捕捉异常
        call_user_func_array('_exception_handler', func_get_args());
    }

}

至此由于修改结构引起的 CI 框架本身的报错就完美解决了。

然而战斗还未结束,同志仍需努力,剩下的 问题 6 和 7 两个小怪该如何解决呢?

添加 app/Services/Controller.php

<?php

namespace App\Services;

/**
 * 框架组件 - 控制器
 *
 * @package App\Services
 */
class Controller
{

    /**
     * 程序执行逻辑
     *
     * @param string $func
     * @param array $args
     * @return \CI_Output
     */
    public function _remap($func, array $args)
    {
        // 排除不存在的方法
        method_exists(app(), $func) OR show_404();

        // 获取程序执行结果
        $output = call_user_func_array([app(), $func], $args);

        // 返回请求结果
        return app()->output->set_output($output);
    }

    /**
     * 常用类库自动加载
     *
     * @param string $name
     * @return mixed
     */
    public function __get($name)
    {
        // 修复 cache 类库
        if ($name === 'cache') {
            app()->load->driver('cache', ['adapter' => 'redis', 'backup' => 'file']);
            return app()->cache;
        }

        // 加载 CI 类库
        if (in_array($name, ['agent', 'cart', 'email', 'encryption', 'form_validation', 'image_lib', 'session', 'unit', 'upload'])) {
            app()->load->library(array_get(['agent' => 'user_agent', 'unit' => 'unit_test'], $name, $name));
            return app()->$name;
        }

        // 加载系统类库
        return load_class($name === 'load' ? 'Loader' : is_loaded()[$name], 'core');
    }

}

这里的 app() 函数是在 functions.php 定义的,以下为 resources/helpers/functions.php 的内容:

<?php

if (empty(function_exists('noFunc'))) {

    function noFunc($name)
    {
        return empty(function_exists($name));
    }

}

if (noFunc('noFile')) {

    function noFile($path)
    {
        return empty(file_exists($path));
    }

}

if (noFunc('app')) {

    function app($name = '')
    {
        if (empty($name)) {
            return $GLOBALS['CI'];
        }

        $model = '\\App\\Models\\' . $name;
        return new $model;
    }

}

if (noFunc('config')) {

    function config($key, $default = null)
    {
        return array_get(get_config(), $key, $default);
    }

}

if (noFunc('env')) {

    function env($key, $default = null)
    {
        return getenv($key) ?: $default;
    }

}

if (noFunc('lang')) {

    function lang($key, $lang = '')
    {
        $lang OR $lang = app()->session->language ?: config('language');
        list($index, $file) = explode('@', $key);
        app()->lang->load($file ?: APP_ENV, $lang, false, false);
        return app()->lang->line($index) ?: $index;
    }

}

至此我们的结构重组就算完事了^__^

什么?还没解决 8. 用户前台、管理后台、api 接口的分组 ?其实不然,我们在编写 config/app.php 的过程中,已经通过

define('ENVIRONMENT', $routing['directory'] = APP_ENV);

赋值 $routing['directory'] 来给控制器设置了分组。

具体如何操作呢?需要在 apache 或 nginx 当中设置环境变量

比如 apache 下

分别添加三个网站
各自设置

admin.xxx.com:
SetEnv APP_ENV admin

api.xxx.com:
SetEnv APP_ENV api

www.xxx.com:
SetEnv APP_ENV user

这样就可以让

共用同一份代码访问不同的 Controller 了。

在这种结构下,后续可以轻松嫁接各种 composer 包,比如说 laravel 的 eloquent 等等。

如果对此感兴趣的话,可以关注下 Github 上的 sinoci 项目,没错,是在求 star ;)

PS: 以上都只是我的最佳实践,希望能带来一丝丝的灵感就好^__^

欢迎各种探讨 ^__^

「真诚赞赏,手留余香」
1 人赞赏
小爝
22 条评论