Git Product home page Git Product logo

yaf.app's Introduction

#Yaf.app

一个基于Yaf MVC的PHP应用程序框架,在工作中使用Yaf加了一些特定程序元素,需要的自取,此项目并非使用Yaf的标准,是个人使用Yaf的总结,用得不对的地方请大神勿喷,并告知小弟,不胜感激。

学习与使用Yaf的PHPer有许多,每个人的“姿势”都不尽相同,这是一种好现象,因为说明都动脑了。本项目没有多少创新,很多程序设计的**与框架支持的功能都来源于其他公开或非公开项目。

##需求

####软件版本

  • PHP-5.4+
  • Redis-2.6.17

####PHP扩展

  • Yaf-2.2.9+
  • XHProf-0.9.4
  • phpredis-2.2.4
  • Yar-1.2.1
  • msgpack-0.5.5
  • pcntl

####INI配置

yaf.ini

extension=yaf.so
yaf.cache_config=1
yaf.use_namespace=1

xhprof.ini

extension=xhprof.so
xhprof.output_dir=/Users/xudianyang/Server/var/run/xhprof

msgpack.ini

extension=msgpack.so

redis.ini

extension=redis.so

yar

extension=yar.so

##特性

  • 消息队列
  • 远程调用(RPC)
  • 支持XHProf性能分析
  • 模块自定义配置及配置可回调
  • 模块之间松耦合
  • 格式化输出(json/Msgpack/serialize/plain/view)
  • 异常捕获

##使用说明

###配置文件机制

####全局配置app.ini

yaf application的ini配置说明请参考yaf手册或者相关文档,下面是本人的配置:

[common]
application.debug=1
application.directory=ROOT_PATH "/" APP_NAME "/"
application.bootstrap=ROOT_PATH "/" APP_NAME "/" Bootstrap.php
application.dispatcher.defaultModule="index"
application.dispatcher.defaultController="index"
application.dispatcher.defaultAction="index"
application.dispatcher.throwException=1
application.modules="Index,Log"
application.view.ext="phtml"

[product:common]
;memcached
application.memcached.0.host=127.0.0.1
application.memcached.0.port=11211
application.memcached.0.weight=50
application.memcached.1.host=127.0.0.1
application.memcached.1.port=11212
application.memcached.1.weight=50
;database
application.database.driver="Pdo_mysql"
application.database.host="127.0.0.1"
application.database.port=3306
application.database.username="root"
application.database.password="123123"
application.database.dbname="test"
application.database.charset="utf8"
;log queue
application.queue.log.switch=1
application.queue.log.tablename=app_error_log
application.queue.log.name=log
application.queue.log.module=Log
application.queue.log.controller=Indexjob
application.queue.log.action=Index
;queue
application.queue.redis.host=127.0.0.1
application.queue.redis.port=6379
;xhprof
application.xhprof.open=1
application.xhprof.namespace=yaf-app
;XHPROF_FLAGS_NO_BUILTINS | XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY
application.xhprof.flags= XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY
application.xhprof.ignored_functions.0=call_user_func
application.xhprof.ignored_functions.1=call_user_func_array

其中:

  • memcached段为memcached的连接信息
  • database段为默认数据库连接信息
  • queue为消息队列配置,queue.log为日志队列的配置,包括日志数据据表名,日志的工作任务Action
  • queue.redis为消息队列储存的redis连接信息
  • xhprof为PHP的分层性能测量分析器的配置信息,默认开启性能分析,会根据xhprof.ini的配置,将分析数据写入到指定目录。

####模块配置文件Import.php

每个模块目录下都可以建立一个config文件夹,在其中创建Import.php,写书模块的一些特殊配置信息,如:

<?php
namespace Index;

use Cache\CachePool;

return array(
    'database_config' => array(
        'driver'    => 'Pdo_mysql',
        'host'      => '127.0.0.1',
        'port'      => 3306,
        'username'  => 'root',
        'password'  => '123123',
        'dbname'    => 'yaf_app',
        'charset'   => 'utf8',
    ),

    'expire'      => 3600,
    'redis'       => function() {
            $storage = CachePool::factory(
                array(
                    'storage'   => 'redis',
                    'namespace' => 'auth:',
                )
            );
            CachePool::register($storage);
            CachePool::get()->setResource(
                array(
                    'host' => '127.0.0.1',
                    'port' => 6379,
                )
            );
            CachePool::get()->getResource();
            return CachePool::get();
    },
);

上述配置文件会自动载入,并将其写入到Yaf\Registry::get('config')Yaf\Registry::get('mount')两个全局对象中;两者的差别是config代表常量的配置,mount代表可回调的配置(callable),如上述的redis配置为一个匿名函数,当使用时通过Yaf\Registry::get('mount')->get('redis')可以得到回调的返回对象,多次Yaf\Registry::get('mount')->get('redis')调用只会执行一次匿名函数。

另外Yaf\Registry::get('config')Yaf\Config\Simple类的实例对象,Yaf\Registry::get('mount')MountManager\MountManager类的实例对象。在进行Core\Factory::db()时,database_config会覆盖全局配置database

###消息队列

由于PHP+Redis实现的消息队列,能相当易于部署与维护,对于小型应用来说此种模式的消息队列足矣解决问题。故这里将php-resque与Yaf进行了整合,并修改部分代码。最终的目的是能够使其与Yaf的命令行模式结合,完成后台执行PHP脚本。关于php-resque的更多介绍请参考:用PHP实现守护进程任务后台运行与多线程

#####1.Worker启动脚本

修改./application/library/Resque/bin/Yaf-cli

!/usr/bin/env /Users/xudianyang/Server/php-5.4/bin/php

修改为php解释器的对应路径

define('ROOT_PATH', '/Users/xudianyang/PhpstormProjects/yaf.app-src');

ROOT_PATH常量修改为应用的主目录

define('ASSETS_URL', 'http://assets.phpboy.net/');

ASSETS_URL常量暂用于程序的资源文件目录,如:js,css等

define('BACKEND_URL', 'http://backend.phpboy.net/');

BACKEND_URL常量表示应用域名,应指向public

#!/usr/bin/env /Users/xudianyang/Server/php-5.4/bin/php

<?php
define('DS',            '/');
define('APP_NAME',      'application');
define('ROOT_PATH',     '/Users/xudianyang/PhpstormProjects/yaf.app-src');
define('INI_PATH',      ROOT_PATH.DS.'conf'.DS.'app.ini');

define('ASSETS_URL',    'http://assets.phpboy.net/');
define('BACKEND_URL',   'http://backend.phpboy.net/');

use Yaf\Loader as InternalLoader;
use Resque\Resque;
use Resque\Resque\Worker;
use Resque\Resque\Log;
use Resque\Resque\Redis;
use Psr\Log\LogLevel;

$loader = InternalLoader::getInstance(ROOT_PATH . DS . APP_NAME . DS . 'library');
spl_autoload_register(array($loader, 'autoload'));

//......

#######启动worker

php Yaf-cli start

支持的参数

  • --host=Redis主机(默认为127.0.0.1)

  • --port=Redis端口(默认为6379)

  • --prefix=Redis Key前缀

  • --database=持久化数据库编号

  • --queue=队列名称(默认*,监听所有的工作任务)

  • --process=进程数(默认1,可以开启多个减少延迟)

  • --blocking=是否阻塞(0、1)

  • --interval=轮循间隔(默认1秒)

  • --pid-path=PHP进程ID文件路径(默认/tmp/resque)

注意:需要php在命令行下运行支持exec函数,所以开启worker时先把exec函数打开,启动worker之后再关闭exec函数。

#######关闭worker

php Yaf-cli stop

支持的参数

  • --queue=队列名称(默认*,监听所有的工作任务)

#######启动示例

➜ application/library/Resque/bin>php Yaf-cli start --process=5 --queue=log
[notice] Starting Worker xudianyang-mac.lan:2447:log
[notice] Starting Worker xudianyang-mac.lan:2446:log
[notice] Starting Worker xudianyang-mac.lan:2448:log
[notice] Starting Worker xudianyang-mac.lan:2450:log
[notice] Starting Worker xudianyang-mac.lan:2449:log

#####2.使用消息队列记录程序异常日志

######1.初始化异常表结构

CREATE TABLE IF NOT EXISTS `app_error_log` (
  `logid` int(10) NOT NULL AUTO_INCREMENT,
  `host` char(50) DEFAULT NULL,
  `uri` char(255) DEFAULT NULL,
  `query` char(255) DEFAULT NULL,
  `module` char(50) DEFAULT NULL,
  `controller` char(255) DEFAULT NULL,
  `action` char(50) DEFAULT NULL,
  `params` text,
  `exception` char(255) DEFAULT NULL,
  `code` int(4) DEFAULT NULL,
  `message` varchar(1000) DEFAULT NULL,
  `file` varchar(500) DEFAULT NULL,
  `line` int(4) DEFAULT NULL,
  `timestamp` int(10) DEFAULT NULL,
  `datetime` char(25) DEFAULT NULL,
  PRIMARY KEY (`logid`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1

######2.使用异常日志——创建日志工作任务

在上述开启了worker之后,我们就可以入队列与出队列,完成工作任务了。本人在使用php-resque与Yaf时,将Yaf命令行路由分发与php-resque的工作任务(job)结合起来使用,即写消息队列的工作任务代码就和写其他正常的Action一样,只不过controller继承内置的\Core\ServiceJob类。

示例代码:./applicaiton/modules/Log/controllers/Indexjob.php

<?php

use Core\ServiceJob;
use Log\LogModel;

class IndexJobController extends ServiceJob
{
    public function init()
    {
        parent::init();
    }
    public function indexAction()
    {
        $data = $this->getRequest()->getParams();
        $log = new LogModel();
        $log->add($data);
    }
}

######3.使用异常日志——消息入队列

示例代码:./public/index.php

<?php
/**
 * Yaf.app Framework
 *
 * @author xudianyang<[email protected]>
 * @copyright Copyright (c) 2014 (http://www.phpboy.net)
 */

define('DS',            '/');
define('ES',            'EXCEPTION-STDERR');
define('APP_NAME',      'application');
define('ROOT_PATH',      realpath(dirname(__FILE__).'/../'));
define('INI_PATH',       ROOT_PATH.DS.'conf'.DS.'app.ini');

define('ASSETS_URL',    'http://assets.phpboy.net/');
define('BACKEND_URL',   'http://backend.phpboy.net/');

use Yaf\Application;
use Yaf\Dispatcher;
use Yaf\Request\Simple as RequestSimple;
use Core\ErrorLog;
use Exception as Exception;
use Resque\Resque;
use Sender\Http as SenderHttp;

if (substr($_SERVER['HTTP_USER_AGENT'], 0, 11) === 'PHP Yar Rpc') {
	//......
} else {
    try {
        $app = new Application(INI_PATH, 'product');
        $app->bootstrap()->run();
    } catch(Exception $e) {
        $sender = new SenderHttp();
        if (Application::app()->getConfig()->application->debug) {
            $sender->setStatus(503, 'Exception: '.$e->getMessage());
        } else {
            $sender->setStatus(503, 'Exception');
        }
        $sender->send();
        if (Application::app()->getConfig()->application->queue->log->switch) {
            $error = new ErrorLog($e, Dispatcher::getInstance()->getRequest());
            $error->errorLog();
        } else {
            echo $e->getMessage();
        }
    }
}

可以看到所有异常都会被捕获,在开启日志队列时,会通过ErrorLog类的errorLog方法进行入队列操作。

<?php
/**
 * Yaf.app Framework
 *
 * @author xudianyang<[email protected]>
 * @copyright Copyright (c) 2014 (http://www.phpboy.net)
 */

namespace Core;

use Exception as Exception;
use Yaf\Request_Abstract;
use Yaf\Application;
use Resque\Resque;

class ErrorLog
{
// ......
    public function errorLog()
    {
        if (isset(Application::app()->getConfig()->application->queue)
            && isset(Application::app()->getConfig()->application->queue->redis)
            && isset(Application::app()->getConfig()->application->queue->log)
        ) {
            $redis_config = Application::app()->getConfig()->application->queue->redis->toArray();
            $server   = $redis_config['host'] . ':'. $redis_config['port'];
            $database = isset($redis_config['database']) ? $redis_config['database'] : null;
            Resque::setBackend($server, $database);
            $args = array(
                'module'     => Application::app()->getConfig()->application->queue->log->module,
                'controller' => Application::app()->getConfig()->application->queue->log->controller,
                'action'     => Application::app()->getConfig()->application->queue->log->action,
                'args'       => $this->toArray(),
            );
            $queue_name = Application::app()->getConfig()->application->queue->log->name;
            Resque::enqueue($queue_name, 'Resque\Job\YafCLIRequest', $args, true);
        }
    }
}

重点在

Resque::setBackend($server, $database);
Resque::enqueue($queue_name, 'Resque\Job\YafCLIRequest', $args, true);

######4.使用异常日志——执行工作任务

./application/library/Resque/Job/YafCLIRequest.php

<?php
/**
 * Yaf.app Framework
 *
 * @author xudianyang<[email protected]>
 * @copyright Copyright (c) 2014 (http://www.phpboy.net)
 */

namespace Resque\Job;

use Yaf\Application;
use Yaf\Dispatcher;
use Yaf\Request\Simple as RequestSimple;
use Core\ErrorLog;
use Resque\Resque;
use Exception as Exception;

class YafCLIRequest
{
    public function perform()
    {
        try {
            $app = new Application(INI_PATH);
            $request = new RequestSimple('CLI', $this->args['module'], $this->args['controller'], $this->args['action'], $this->args['args']);
            $app->bootstrap()->getDispatcher()->dispatch($request);
        } catch(Exception $e) {
            if (Application::app()->getConfig()->application->queue->log->switch) {
                $error = new ErrorLog($e, Dispatcher::getInstance()->getRequest());
                $error->errorLog();
            }
        }
    }
}

worker进程在检测队列时,如果队列不为空,就会依次进行出队列的操作,运行YafCLIRequest::perform()方法,通过Yaf命令行下的路由分发,完成工作任务。

######5.使用异常日志——触发异常

./application/modules/Demo/controllers/Index.php

<?php

use Yaf\Controller_Abstract;
use Yaf\Dispatcher;
use Yar\YarClient;
class IndexController extends Controller_Abstract
{
    public function indexAction()
    {
        // 空的,加载一下模板
    }

    public function testYarApiAction()
    {
        Dispatcher::getInstance()->disableView();
        $client = new YarClient(
            array(
                'module' => 'demo',
                'controller' => 'demoapi',
                'action' => 'getdata',
            ),
            array('args' => 'some parameters', 'format' => 'json')
        );
        $data = $client->api();
        print_r($data);
    }

    public function testLogAction()
    {
        // 空的,没有对应的模板,此处抛出的异常应被log/indexjob/index捕获并写入相应的储存介质
    }
}

看到testLogAction,当访问:

http://backend.phpboy.net/demo/index/testlog

就会抛出异常,因为程序找不到与之对应的模板文件。

Failed opening template /Users/xudianyang/PhpstormProjects/yaf.app-src/application/Modules/Demo/Views/index/testlog.phtml: No such file or directory

查看日志表,如果数据库连接信息配置得当,就会出现一条日志信息。

###远程调用(Yar轻量级RPC)

大家都知道,在以前的PHP开源或者闭源程序中,经常我们看到A模块直接加载B模块的Model,从而使得这A、B两个模块之间耦合度高,没有办法拆分或者单独部署,这样整个项目的维护与扩展就越来越困难。为了解决这样的问题,本人采用Yar+Yaf的路由分发实现远程调用,完成模块之间的数据调用,并且我们可以和写普通Action一样导出API,只需controller继承Core\ServiceApi内置类。

#####1.导出API

./application/modules/Demo/controllers/Demoapi.php

<?php

use Core\ServiceApi;

class DemoApiController extends ServiceApi
{
    public function init()
    {
        parent::init();
    }

    public function getDataAction()
    {
        $this->sendOutput("这是通过远程调用返回的数据(Yar)传递的参数: args => " . $this->getRequest()->getParam('args'));
    }
}

导出的API需要正常输出相应的格式数据。

#####2.调用API

./application/modules/Demo/conrollers/Index.php

<?php

use Yaf\Controller_Abstract;
use Yaf\Dispatcher;
use Yar\YarClient;
class IndexController extends Controller_Abstract
{
    public function indexAction()
    {
        // 空的,加载一下模板
    }

    public function testYarApiAction()
    {
        Dispatcher::getInstance()->disableView();
        $client = new YarClient(
            array(
                'module' => 'demo',
                'controller' => 'demoapi',
                'action' => 'getdata',
            ),
            array('args' => 'some parameters', 'format' => 'json')
        );
        $data = $client->api();
        print_r($data);
    }

    public function testLogAction()
    {
        // 空的,没有对应的模板,此处抛出的异常应被log/indexjob/index捕获并写入相应的储存介质
    }
}

看到testYarApiAction,实例化一个Yar\YarClient类。

其中第一个参数代表导出的API的MVC信息,这里为:

array(
	'module' => 'demo',
	'controller' => 'demoapi',
	'action' => 'getdata',
)

对应上述导出的API

其中第二个参数代表请求相应的参数,args代表传递的所有参数,format为请求间传递数据的格式,默认为json,还可以为serialize、plain。

最后调用Yar\YarClient类实例对象的api方法,完成请求并返回相应数据。

访问:http://backend.phpboy.net/demo/index/testyarapi

输出

这是通过远程调用返回的数据(Yar)传递的参数: args => some parameters

###XHProf的使用

在开启PHP性能分析时,可以将每次请求的分析数据保存到xhprof.output_dir目录中,这里只保存了分析数据,如果要查看相信的分析信息,需要xhprof_html和xhprof_lib,这是一个PHP实现的界面,使得查看XHProf分析结果变得更加容易。更多XHProf的详细介绍:PHP性能分析工具xhprof介绍、安装、使用说明

另外需要说明的是要修改xhprof_lib/utils/xhprof_lib.php中

function xhprof_param_init($params) {
  /* Create variables specified in $params keys, init defaults */
  foreach ($params as $k => $v) {
    switch ($v[0]) {
    case XHPROF_STRING_PARAM:
      $p = xhprof_get_string_param($k, $v[1]);
      break;
    case XHPROF_UINT_PARAM:
      $p = xhprof_get_uint_param($k, $v[1]);
      break;
    case XHPROF_FLOAT_PARAM:
      $p = xhprof_get_float_param($k, $v[1]);
      break;
    case XHPROF_BOOL_PARAM:
      $p = xhprof_get_bool_param($k, $v[1]);
      break;
    default:
      xhprof_error("Invalid param type passed to xhprof_param_init: "
                   . $v[0]);
      exit();
    }

    if ($k === 'run') {
      // 这里需要改动
      //$p = implode(',', array_filter(explode(',', $p), 'ctype_xdigit'));
      $p = implode(',', explode(',', $p));
    }

    // create a global variable using the parameter name.
    $GLOBALS[$k] = $p;
  }
}

yaf.app's People

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.