用Composer搭建PHP框架(二)

首先给我们的框架起个名字叫“Poppy”,然后在自己工作目录新建一个Poppy文件夹,然后在该文件夹下新建src目录,这是本次存放我们自己代码的文件夹,然后在src下新建public文件夹并且在其内部新建index.php:

mkdir -p src/public 
cd src/public
touch index.php

文件夹public属于nginx对外可以访问的,index.php作为我们的框架入口文件
==注意:本次演示的环境是基于mac+php7.0==

mkdir Poppy && cd Poppy`

我们把Poppy这个目录当成我们的工作目录,我先大概列出我们的目录,我大概建了几个主要文件,使用tree -L 3查看如下:

.
├── composer.json
├── composer.lock
├── src
│   ├── Core //核心目录
│   │   ├── App.php 
│   │   └── Handler.php
│   ├── Log //日志文件夹
│   │   └── poppy.log
│   ├── Model //模型文件夹
│   │   ├── Content.php
│   │   └── Model.php //model基类
│   ├── Public
│   │   └── index.php //项目单入口文件
│   ├── Services //控制器文件夹
│   │   └── articleController.php
│   ├── config.json //配置文件
│   └── routes.php //路由文件

接下来我们先使用Composer添加我们的HTTP组件,在命令行敲入

composer require symfony/http-foundation

回车等待执行完成,好了结果如下:

Using version ^3.2 for symfony/http-foundation
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-mbstring (v1.3.0): Loading from cache
  - Installing symfony/http-foundation (v3.2.8): Loading from cache
Writing lock file
Generating autoload files

composer所有的东西都存放在vendor里面,包括我们之前讲到的autoload.php,以及我们引入的symfony/http-foundation包,接下来我们引入该包作为我们的HTTP组件。来验证一下,写入以下代码:

// at Public/index.php

require __DIR__ . "/../../vendor/autoload.php";

define('ROOT_PATH', __DIR__);
define('APP_PATH', __DIR__ . "/../");
//创建一个request请求
$req = Symfony\Component\HttpFoundation\Request::createFromGlobals();
//获取该请求的方法类型
echo "Method: ".$req->getMethod()."
"
; //获取URI的值 echo "URI: " . $req->getRequestUri() . "
"
; //获取a参数的值,第二个参数为当a不存在时的默认值 echo "Params: ".$req->get('a','default');

使用php自带webserver在项目目录下启动项目:

php -S 127.0.0.1:8001 -t ./src/public

使用浏览器访问网址:http://127.0.0.1:8001/users/list?a=1返回以下结果:

Method: GET
URI: /users/list?a=1
Params: 1

好了,体验了一把,我们终于可以直接使用别人的组件了,感觉加快了开发步骤,是不是感觉很爽~,有些人很奇怪,为什么就是这么写呢?为什么是这么用呢?其实用的方法有很多,当你拿到一个包你首先要阅读完全它的文档说明,只有知道它都有些什么,才能更好的利用它。

接下来继续添加我们的路由组件,这里选用nikic/fast-route

composer require nikic/fast-route

成功的界面不在赘述了,通过阅读它的usage我们写出如下代码,首先在src里面新建routes.php写入一些测试的路由:

//at routes.php
return [
    //homepage
    [
        ['GET'],
        '/',
        'App\Services\index@index'
    ],
    //create articles
    [
        ['POST'],
        '/articles',
        'App\Services\article@create'
    ],
    //delete articles
    [
        ['DELETE'],
        '/articles/{Id}',
        'App\Services\article@delArticleById'
    ],
    //articles list
    [
        ['GET'],
        '/articles',
        'App\Services\article@articlesList'
    ]
];

它返回的是一个大数组,数组内第一个值为请求方法,第二个为请求的URI,第三个为请求对应到的具体方法,接下来来完善我们的项目

// at Core/App.php


namespace App\Core;

class App
{
    use Handler;

    public function run()
    {
        $this->initRoute();
    }
}
// at Core/Handle.php


namespace App\Core;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Odan\Middleware\Dispatcher\HttpFoundationSprinter;

trait Handler
{
    /**
     * Init route
     * @return $this|mixed
     */
    public function initRoute()
    {
        $routes = require APP_PATH . "/routes.php";


        $dispatcher = \FastRoute\simpleDispatcher(function (\FastRoute\RouteCollector $r) use ($routes) {
            foreach ($routes as $route) {
                $methods = array_map("strtoupper", (array)$route[0]);
                $r->addRoute($methods, $route[1], $route[2]);
            }
        });

        $request = Request::createFromGlobals();
        $method = $request->getMethod();
        $uri = rawurldecode($request->getRequestUri());
        if (false !== $pos = strpos($uri, '?')) {
            $uri = substr($uri, 0, $pos);
        }

        $response = Response::create();

        //注入了Logger之后可以去除注释
        //App::Logger()->addInfo(
        //    sprintf(
        //        'Accepted request %s %s',
        //        $request->getMethod(),
        //        $request->getUri()
        //    ),
        //    [
        //        $request->getQueryString()
        //    ]
        //);

        $routeInfo = $dispatcher->dispatch($method, $uri);
        switch ($routeInfo[0]) {

            case \FastRoute\Dispatcher::NOT_FOUND:
                return $response->setStatusCode('404')->setContent("404 Not Found")->send();
                break;
            case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
                $allowedMethods = $routeInfo[1];
                return $response->setStatusCode('405')->setContent("405 Method Not Allowed")->send();
                break;
            case \FastRoute\Dispatcher::FOUND:

                try {

                    $sprinter = new HttpFoundationSprinter();
                    $response = $sprinter->run($request, $response, []);

                    $handler = $routeInfo[1];
                    $vars = $routeInfo[2];
                    if (is_string($handler) && (strpos($handler, '@'))) {

                        $ret = $this->callHandler($handler, $vars, $request, $response);
                    } else {
                        $ret = call_user_func($handler, $request, $response, $vars);
                    }
                } catch (\Exception $e) {
                    return $response->setStatusCode("500")->setContent($e->getMessage())->send();
                }

                if ($ret instanceof Response) {
                    return $ret;
                }
                if (is_array($ret)) {
                    $response->headers->set("Content-Type", "application/json;charset=utf-8");
                    return $response->setContent(json_encode($ret))->send();
                }
                return $response->setContent($ret)->send();
                break;
        }
    }

    /**
     * @param $handler
     * @param $vars
     * @param $request
     * @param $response
     * @return mixed
     */
    public function callHandler($handler, $vars, $request, $response)
    {
        if (is_string($handler) && (strpos($handler, '@'))) {
            list($class, $method) = explode('@', $handler);
            $class = ucfirst($class) . "Controller";
            return (new $class())->$method($request, $response, $vars);
        }

        return call_user_func($handler, $request, $response, $vars);
    }
}

通过以上代码,我们能把routes里面的定义的路由转发到我们对应的控制器里面去,接下来我们来创建我们的controller,我们在src里面创建一个services文件夹,在里面创建一个articleController.php,随便写入一些内容如下:


//at Services/articleController.php

namespace App\services;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class articleController
{
    public function articlesList(Request $request, Response $response)
    {
        return ['msg'=>'success'];
    }
}

记得在index.php去调用App类里面的run方法:

//at Public/index.php

//实例化核心app类
$app = new \App\Core\App();
//启动框架
$app->run();

好了。我们访问http://127.0.0.1:8001/articles页面,看到页面返回一个json内容:

{
    'msg'=>'success'
}

到此为止,好像我们”弄”出了一个大概能用的框架了,能通过url定位到controller,是不是感觉略屌~,不过路还很长,继续往下看吧。

添加服务容器并且注入配置文件服务、日志服务

在项目目录执行

//引入服务容器
composer require pimple/pimple

//引入配置文件读取包
composer require hassankhan/config

//引入著名的 monolog 日志服务
composer require monolog/monolog

//引入ORM
composer require illuminate/database

App.php里面使用pimple来注入我们的服务,使用它很简单,注入一个服务就像添加一个服务后缀一样方便【服务是一个对象,当然你也可以使用它的工厂方法每次返回新的对象】,


//at App.php

namespace App\Core;

use Illuminate\Database\Capsule\Manager;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Noodlehaus\Config;
use Pimple\Container;

/**
 * Class App
 * @package App\Core
 */
class App
{
    use Handler;

    /**
     * @var $container
     */
    protected static $container;

    /**
     * @var $config
     */
    protected static $config;

    /**
     * Init container
     */
    public function initContainer()
    {

        $container = new Container();

        //载入我们的config文件
        $config = Config::load(APP_PATH . "config.json");

        //注入到容器,下次可以直接使用
        $container['config'] = $config;

        //日志服务代码如下,我们使用config作为闭包的参数传进去
        $container['logger'] = function () use ($config) {
            $logger = new Logger($config->get('app_name'));
            $logger->pushHandler(new StreamHandler($config->get('log_file')));
            return $logger;
        };

        //载入 DB
        $capsule = new Manager();
        foreach ($config->get('connections') as $name => $item) {
            $capsule->addConnection([
                'driver'    => $item['driver'],
                'host'      => $item['host'],
                'database'  => $item['name'],
                'username'  => $item['username'],
                'password'  => $item['password'],
                'charset'   => $item['charset'],
                'collation' => $item['collation'],
                'prefix'    => $item['prefix'],
            ], $name);
        }
        $capsule->setAsGlobal();
        $capsule->bootEloquent();

        //赋值给静态类属性,方便下次使用
        self::$container = $container;

    }

    /**
     * @return mixed
     */
    public static function getContainer()
    {
        return self::$container;
    }

    /**
     * @return \Monolog\Logger
     */
    public static function Logger()
    {
        return self::$container['logger'];
    }

    /**
     * @return \Noodlehaus\Config
     */
    public static function Config()
    {
        return self::$container['config'];
    }


    /**
     * start app
     */
    public function run()
    {
        $this->initContainer();
        $this->initRoute();
    }
}

config.json 文件内容如下:

{
  "app_name": "Poppy",
  "log_file": "../log/poppy.log",
  "connections": {
    "default": {
      "driver": "mysql",
      "host": "localhost",
      "username": "root",
      "password": "123456",
      "port": "3306",
      "name": "gear",
      "charset": "utf8",
      "collation": "utf8_general_ci",
      "prefix": ""
    }
  }
}

载入了orm,就要去使用它,我们在Model/Content.php添加如下代码:

//at Model/Content.php


namespace App\Model;

class Content extends Model
{
    //自定义表名
    protected $table = 'content';
}

Model.php作为model基类来集成我们的Eloquent ORM类

//at Model/Model.php



namespace App\Model;

class Model extends \Illuminate\Database\Eloquent\Model
{

}

model准备好了,我们在Controller里面去使用它,大概就是返回某一条数据(方法命名好像不太合理不管了~)

//at articleController.php



namespace App\services;

use App\Model\Content;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class articleController
{

    private $container;

    /**
     * 通过构造函数传入的服务容器实体
     * 也可以直接通过App::getContainer()获取 
     * articleController constructor.
     */
    public function __construct($container)
    {
        $this->container = $container;
    }

    public function articlesList(Request $request, Response $response, $vars)
    {

        //获取一条内容
        $rel = Content::first();
        $data = [];
        //如果存在则格式化数组返回
        if ($rel) {
            $data = $rel->toArray();
        }

        return ['msg' => 'success', 'data' => $data];
    }
}

访问http://127.0.0.1:8001/articles可以看到页面显示如下:

{
    msg: "success",
    data: {
        id: 1,
        name: "Name1"
    }
}

查看Log文件夹下自动生成了poppy.log,那代表我们的Log也能成功运行。

yeah,是不是感觉很有意思,到此为止,我们实现了路由、服务容器、日志、配置文件,这些应该满足了80%的业务需求,当然还不够,接下来我会继续更新,一一实现我们之前我们所讲的那些组件。

你可能感兴趣的:(PHP)