Overview

Biny is a high performance lightweight PHP framework.

It follows the MVC pattern for rapid development of modern Web applications

Biny code is simple and elegant. The application layer, data layer, and template rendering layer of the package is simple and easy to understand. This makes it quick to pick up.

Biny is high performance. Framework comes default with response time of less than 1ms. Stand-alone QPS easily up to 3000.

Introduce

Support cross library join table, conditional compound filter, query PK cache, etc.

Synchronous asynchronous request separation, automatic loading management of classes

Supports Form validation and supports event triggering mechanisms

Supports browser side debugging, rapid positioning problems and performance bottlenecks

With SQL anti injection, HTML automatic anti XSS and other characteristics

Frameword Wiki:http://www.billge.cc

GitHub url:https://github.com/Tencent/Biny

Directory

/app/ Top directory

/app/config/ App config layer

/app/controller/ Controller Action layer

/app/dao/ Database table layer

/app/event/ Event layer

/app/form/ Form layer

/app/model/ Model layer

/app/service/ Service layer

/app/template/ View Template layer

/config/ Lib config layer

/lib/ System lib layer

/extends/ Custom lib layer

/logs/ Log direcory

/web/ Executing entry directory

/web/static/ Static resource directory

/web/index.php Executing entry file

/shell.php Shell model start file

Call relation

Action is the general routing entry, and Action can call the private object Service business layer and the DAO database layer

Service business layer can call private object DAO database layer

The program can call the system method under the Lib library, such as Logger (debug component)

App::$baseis a global singleton class, which can be called globally

App::$base->request is the current request, access to the current address, client IP, etc.

App::$base->session is the system session, can be directly obtained and copied, set the expiration time

App::$base->memcache is the system Memcache, can be directly obtained and copied, set the expiration time

App::$base->redis is the system redis, can be directly obtained and copied, set the expiration time

Users can customize the model data class under /app/model/, and get them through App::$model, for example:

App::$model->person is the current user,It can be defined in /app/model/person.php

Simple example

namespace app\controller;
use App;
/**
* main Action
* @property \app\service\projectService $projectService
* @property \app\dao\projectDAO $projectDAO
*/  
class testAction extends baseAction
{
    // The init method is executed before the action is executed
    public function init()
    {
        // Login page adjustment when not logged in
        if(!App::$model->person->exist()){
            return App::$base->response->redirect('/auth/login/');
        }
    }

    // Default routing index
    public function action_index()
    {
        //  Get current user
        $person = App::$model->person;
        $members = App::$base->memcache->get('cache_'.$person->project_id);
        if (!$members){
            // Get the members of the user's project
            $project = $this->projectDAO->find(array('id'=>$person->project_id));
            $members = $this->projectService->getMembers($project['id']);
            App::$base->memcache->set('cache_'.$person->project_id, $members);
        }
        // return project/members.tpl.php
        return $this->display('project/members', array('members'=>$members));
    }
}

P.S: The usage of the example will be described in detail below

Environmental allocation

The PHP version must be more than 5.5, including 5.5

If you need to use the database, you need to install and enable the mysqli expansion

In php.ini you needed to set the short_open_tag On

/config/autoload.php is the automatic loading file, must have write permissions

/logs/ is the log folder, also must have write permissions

This example describes the Linux nginx configuration

Root needs to point to the /web/ directory, for example:

location / {
    root   /data/billge/biny/web/; // Here is the absolute path of the framework /web directory
    index  index.php index.html index.htm;
    try_files $uri $uri/ /index.php?$args;
}

The configuration of Apache is as follows::

# Set the document root to the /web directory
DocumentRoot "/data/billge/biny/web/"

<Directory "/data/billge/biny/web/">
    RewriteEngine on
    # If the request is the existence of a file or directory, direct access
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # If the request is not the real file or directory, distribute requests to index.php
    RewriteRule . index.php

    # ...other settings...  
</Directory> 

/web/index.php Is the main entrance program, which has several key configuration

// Default timezone configuration
date_default_timezone_set('Asia/Shanghai');
// Open debug mode (output exception)
defined('SYS_DEBUG') or define('SYS_DEBUG', true);
// Open Logger debugging in browser console
defined('SYS_CONSOLE') or define('SYS_CONSOLE', true);
// dev pre pub environment
defined('SYS_ENV') or define('SYS_ENV', 'dev');
// System maintenance
defined('isMaintenance') or define('isMaintenance', false);

The SYS_ENV values in the environment also has bool, convenient use of judgment

// \lib\App.php 
// Devnet
defined('ENV_DEV') or define('ENV_DEV', SYS_ENV === 'dev');
// Pre release
defined('ENV_PRE') or define('ENV_PRE', SYS_ENV === 'pre');
// Release
defined('ENV_PUB') or define('ENV_PUB', SYS_ENV === 'pub');

Route

The basic architecture of MVC routing model, corresponding to the first layer action, second layer corresponding to method (default index)

Default rule

In the /app/controller directory, the file can be placed in any directory or directory in the sun. But must ensure that the file name is consistent with the class name, and not repeat

example:/app/controller/Main/testAction.php

// http://www.billge.cc/test/
class testAction extends baseAction
{
    //default route index
    public function action_index()
    {
        //return test/test.tpl.php
        return $this->display('test/test');
    }
}

At the same time also can configure multiple sub routing in the same file

//sub routing find method action_{$router}
// http://www.billge.cc/test/demo1
public function action_demo1()
{
    //return test/demo1.tpl.php
    return $this->display('test/demo1');
}

// http://www.billge.cc/test/demo2
public function action_demo2()
{
    //return test/demo2.tpl.php
    return $this->display('test/demo2');
}

Custom route

In addition to the default routing, the routing rules can be customized and configured in /config/config.php

The custom routing rules are executed first, then the default rules are taken after the matching fails, and the strings after the parameter colons are automatically converted into regular matcher

/config/config.php
'routeRule' => array(
    // test/(\d+).html will be automatically forwarded to the action_view method in testAction
    
    'test/<id:\d+>.html' => 'test/view',
    // The matching parameters can be used in dynamic routing forwarding
    'test/<method:[\w_]+>/<id:\d+>.html' => 'test/<method>',
),

/app/controller/testAction.php
// test/272.html Regular match content is introduced into the method
public function action_view($id)
{
    echo $id; // 272
}

// test/my_router/123.html
public function action_my_router($id)
{
    echo $id; // 123
}

Ajax request

The asynchronous request contains POST, Ajax and many other requests, and the system automatically performs CSRF and processing

The response method and the synchronization request are consistent in the program, and the return $this->error() will automatically differentiate from the synchronous request and return the JSON data

// http://www.billge.cc/test/demo3
public function action_demo3()
{
    $ret = array('result'=>1);
    //return json {"flag": true, "ret": {"result": 1}}
    return $this->correct($ret);

    //return json {"flag": false, "error": {"result": 1}}
    return $this->error($ret);
}

The framework provides a full set of CSRF authentication mechanisms that are opened by default and can be closed by $csrfValidate = false in Action.

// http://www.billge.cc/test/
class testAction extends baseAction
{
    // close CSRF authentication
    protected $csrfValidate = false;

    // default route index
    public function action_index()
    {
        //return test/test.tpl.php
        return $this->correct();
    }
}

When the CSRF validation is opened, the front-end Ajax request needs to preload reference /static/js/main.js file, and when the Ajax commits, the system adds the validation field automatically.

The POST request will also trigger CSRF verification, need to add the following data fields in form:

// add in form
<input type="text" name="_csrf" hidden value="<?=$this->getCsrfToken()?>"/>

You can also get it in JS (the premise is to refer to the /static/js/main.js file) and add it to the POST parameter.

var _csrf = getCookie('csrf-token');

Restful

Biny also supports the request of the restful protocol, and $restApi can be set to true in the Action class, and the Action will parse the routing with the protocol of restful

namespace app\controller;
/**
 * restful demo
 * @property \app\dao\userDAO $userDAO
 */
class restAction extends baseAction
{
    // The action analyzes routing with the restful protocol
    protected $restApi = true;

    // [GET] http://www.billge.cc/rest/?id=xxx
    public function GET_index($id)
    {
        $user = $this->userDAO->filter(['id'=>$id])->find();
        return $user ? $this->correct($user) : $this->error('user not found');
    }

    // [POST] http://www.billge.cc/rest/test
    public function POST_test()
    {
        $user = $this->param('user');
        $user_id = $this->userDAO->add($user);
        return $user_id ? $this->correct($user) : $this->error('data error');
    }

    // [PUT] http://www.billge.cc/rest/?id=xxx
    public function PUT_index($id)
    {
        $user = $this->param('user');
        $ret = $this->userDAO->filter(['id'=>$id])->update($user);
        return $ret ? $this->correct() : $this->error('data error');
    }

    // [PATCH] http://www.billge.cc/rest/test?id=xxx
    public function PATCH_test($id)
    {
        $sets = $this->param('sets');
        $ret = $this->userDAO->filter(['id'=>$id])->update($sets);
        return $ret ? $this->correct() : $this->error('data error');
    }

    // [DELETE] http://www.billge.cc/rest/test?id=xxx
    public function DELETE_test($id)
    {
        $ret = $this->userDAO->filter(['id'=>$id])->delete();
        return $ret ? $this->correct() : $this->error('data error');
    }
}

Similarly, the restful protocol can also be configured through custom routing mode, for example:

/config/config.php
'routeRule' => array(
    // rest/(\d+) routing will be automatically forwarded to the {method}_test method in restAction
    'rest/<id:\d+>' => 'rest/test',
    // The matching parameters can be used in dynamic routing forwarding
    'v<version:\d+>/rest/<id:\d+>/<method:[\w_]+>' => 'rest/<method>',
),

/app/controller/restAction.php
// [DELETE] http://www.billge.cc/v2/rest/123/person
public function DELETE_person($version, $id)
{
    echo $version; // 2
    echo $id; // 123
}
// [PUT] http://www.billge.cc/rest/272 Regular match content is introduced into the method
public function PUT_test($id)
{
    echo $id; // 272
}

Get Param

The method can directly receive GET parameters, and can be assigned a default value, empty returns null

// http://www.billge.cc/test/demo4/?id=33
public function action_demo4($id=10, $type, $name='biny')
{
    // 33
    echo($id);
    // NULL
    echo($type);
    // 'biny'
    echo($name);
}

At the same time, you can also call param, get, post method to obtain the parameters.

param($key, $default) Get the GET/POST/JSON parameter of {$key}, the default value is {$default}

get($key, $default) Get the GET parameter of {$key}, the default value is {$default}

post($key, $default) Get the POST parameter of {$key}, the default value is {$default}

getJson($key, $default) Get the JSON data with {$key} by row input json flow, the default value is {$default}

// http://www.billge.cc/test/demo5/?id=33
public function action_demo5()
{
    // NULL
    echo($this->param('name'));
    // 'install'
    echo($this->post('type', 'install'));
    // 33
    echo($this->get('id', 1));
}

Authorization verification

The framework provides a complete set of permissions validation logic for authorization of routing all method

You need to add the privilege method in action, the specific field return as follows

class testAction extends baseAction
{
    private $key = 'test';

    protected function privilege()
    {
        return array(
            // login validation(define in privilegeService)
            'login_required' => array(
                'actions' => '*', // bind actions, * for all methods
                'params' => [],   // params (can access $this) not have to send
                'callBack' => [], // callback when failed, not have to send
            ),
            'my_required' => array(
                'actions' => ['index'], // bind action_index
                'params' => [$this->key],   // send $this->key
                'callBack' => [$this, 'test'], // call $this->test() when failed
            ),
        );
    }
    // After login_required and my_required are verified success, the method will be called
    public function action_index()
    {
        // do something
    }
    // called after failed of my_required validation, param $action is the current action object
    public function test($action, $error)
    {
        // do something
    }
}

Then define the validation method in privilegeService

The first parameter $action is testAction, $key is the params argument
public function my_required($action, $key=NULL)
{
    if($key){
        // pass
        return $this->correct();
    } else {
        // fail, error message can get by $this->privilegeService->getError()
        return $this->error('key not exist');
    }
}

callBack is the method called when the check fails. then throw out the error exception, and the program will not continue to execute.

If different routes need the same authentication method, and need send the different parameters. The requires parameter can be used, and the example is used for reference:

class testAction extends baseAction
{
    protected function privilege()
    {
        return array(
            'privilege_required' => array(
                // The corresponding operation permissions are introduced according to different routes
                'requires' => [
                    ['actions'=>['index', 'view'], 'params'=>[Privilege::user]],
                    ['actions'=>['edit', 'delete'], 'params'=>[Privilege::admin]],
                ],
                'callBack' => [$this, 'test'], // called $this->test() when check failed
            ),
        );
    }

// privilegeService
public function privilege_required($action, $privilege)
{
    if(App::$model->person->hasPrivilege($privilege)){
        // the user has the privilege
        return $this->correct();
    } else {
        // check failed, send the error message
        return $this->error('forbidden');
    }
}

Note: when you use the requires parameter actions and params parameter will be covered

Config

The configuration is divided into two blocks, one is the system configuration, one is the application configuration

/config/ System configuration path

/app/config/ Application configuration path

System Config

/config/config.php System common configuration (Including the default routing, custom routing configuration etc.)

return array(
    // route configuration
    'router' => array(
        'base_action' => 'demo', // default action entry
        'base_shell' => 'index', // default shell entry

        // Static configuration
        'routeRule' => array(
            // test/123 => test/view
            'test/<id:[\w_]+>' => 'test/view',
            // abc/test/123 => test/abc
            '<method:\w+>/test/<id:\d+>.html' => 'test/<method>',
        ),
    ),

    // autoload configuration
    'autoload' => array(
        'autoPath' => 'config/autoload.php',
        // refresh autoload skip seconds
        'autoSkipLoad' => 5,
        'autoThrow' => true, // when use of an external autoload mechanism (such as composer) should be set to false
    ),

    // request configuration
    'request' => array(
        'trueToken' => 'biny-csrf',
        'csrfToken' => 'csrf-token',
        'csrfPost' => '_csrf',
        'csrfHeader' => 'X-CSRF-TOKEN',

        // get userip cookie key
        'userIP' => '',
        // return tpl when use this cookie in ajax
        'showTpl' => 'X_SHOW_TEMPLATE',
        //csrf white list
        'csrfWhiteIps' => array(
            '127.0.0.1/24'
        ),
        // control language cookie
        'languageCookie' => 'biny_language'
    ),

    // response configuration
    'response' => array(
        'jsonContentType' => 'application/json',
        // Compatible with old versions
        'paramsType' => 'one',  // one or keys
        // param name in tpl
        'paramsKey' => 'PRM',
        'objectEncode' => true, // escaped object param
    ),

    // log configuration
    'logger' => array(
        // enabled write file
        'files' => true,
        // custom message log
//        'sendLog' => array('Common', 'sendLog'),
        // custom error log callback function
//        'sendError' => array('Common', 'sendError'),
        // error level, will trigger above NOTICE
        'errorLevel' => NOTICE,
        // SQL slow query threshold(ms)
        'slowQuery' => 1000,
    ),

    // database configuration
    'database' => array(
        'returnIntOrFloat' => true, // return int or float in select command
        'returnAffectedRows' => false, // return affected rows in update/delete command, -1 means failed
    ),

    // cache configuration
    'cache' => array(
        'pkCache' => 'tb:%s',
        'session' => array(
            'save_handler'=>'files',  //redis memcache
            'maxlifetime' => 86400    // expire seconds
        ),
        // enabled redis serialize
        'serialize' => true,
    ),

    //exception configuration
    'exception' => array(
        // return tpl when error
        'exceptionTpl' => 'error/exception',
        'errorTpl' => 'error/msg',

        'messages' => array(
            500 => 'Some Error in page, please try latter',
            404 => 'Page not found',
            403 => 'Request forbidden, please connect web admin'
        )
    ),



)

/config/autoload.php The configuration of the system automatic loading class will be generated automatically according to user code, without configuration, but must have write permissions

/config/exception.php System Exception configuration

/config/http.php HTTP code

/config/database.php DAO mapping configuration

User can get by the method App:: $base->config->get

for example:

/config/config.php
return array(
    'session_name' => 'biny_sessionid'
}

// The second parameter is the file name (default is config), the third parameter is whether to use alias (default is true)
App::$base->config->get('session_name', 'config', true);

App config

Application configuration directory is /app/config/

Default has dns.php(connect config) 和 config.php(default path config)

The mode of use is basically consistent with the system configuration

/app/config/dns.php
return array(
    'memcache' => array(
        'host' => '10.1.163.35',
        'port' => 12121
    )
}

// The second parameter is the file name (default is config), the third parameter is whether to use alias (default is true)
App::$base->app_config->get('memcache', 'dns');

Environmental allocation

The system of different environment configuration can be distinguished

Environment can configure in /web/index.php

// dev pre pub 
defined('SYS_ENV') or define('SYS_ENV', 'dev');

When you use App::$base->config->get, system will automatically find the corresponding configuration file

// when 'dev' will find file /config/config_dev.php
App::$base->config->get('test', 'config');

// when 'pub' will find file  /config/dns_pub.php文件
App::$base->config->get('test2', 'dns');

Public configuration files can be placed in files that do not add environment names like /config/config.php

When coexist config.php and config_dev.php, The contents of the file with the environment configuration will cover the general configuration

/app/config/dns.php
return array(
    'test' => 'dns',
    'demo' => 'dns',
}

/app/config/dns_dev.php
return array(
    'test' => 'dns_dev
}

// return 'dns_dev' 
App::$base->app_config->get('test', 'dns');

// return 'dns' 
App::$base->app_config->get('demo', 'dns');

System configuration and Application configuration are used in the same way

Alias

Support alias configuration used, can be in the alias on both sides with @

The system has a default alias web will replace the current path

/config/config.php
return array(
    'path' => '@web@/my-path/'
}

// return '/biny/my-path/' 
App::$base->config->get('path');

Users can also customize define alias, for example:

// before method config->get 
App::$base->config->setAlias('time', time());

// config.php
return array(
    'path' => '@web@/my-path/?time=@time@'
}

// return '/biny/my-path/?time=1461141347'
App::$base->config->get('path');

// return '@web@/my-path/?time=@time@'
App::$base->config->get('path', 'config', false);

Of course, if you want to avoid alias escape, you can send false as third parameters in method App::$base->config->get.

Database

Biny requires that each database table need to build a separate class and put it under the /dao directory. Like other directories, support multi tier file structures, you can place file in subdirectories or sun directories, but class names must be unique.

All parameters imported into the DAO method are automatically escaped, and the risk of SQL injection can be completely avoided

for example:

// testDAO.php    Match the class name
class testDAO extends baseDAO
{
    // Database name, Array mains master-slave separation:['database', 'slaveDb'] match the config in dns.php. default is 'database'
    protected $dbConfig = 'database';
    // table name
    protected $table = 'Biny_Test';
    // primary key, array mains double key:['id', 'type']
    protected $_pk = 'id';
    // enable cacke used, default false
    protected $_pkCache = true;

    // Splitting Table method, default method is add id instead
    public function choose($id)
    {
        $sub = $id % 100;
        $this->setDbTable(sprintf('%s_%02d', $this->table, $sub));
        return $this;
    }
}

Connect

Database information configuration in /app/config/dns.php, also can configuration with environment in dns_dev.php/dns_pre.php/dns_pub.php

Common parameters include:

/app/config/dns_dev.php
return array(
    'database' => array(
        // IP
        'host' => '127.0.0.1',
        // Database name
        'database' => 'Biny',
        // User name
        'user' => 'root',
        // Password
        'password' => 'pwd',
        // Code format
        'encode' => 'utf8',
        // Port
        'port' => 3306,
        // Enable keep alive (default false)
        'keep-alive' => true,
    )
)

You can also configure multiple, and you need configuration parameter '$dbConfig' in the DAO class (default is 'database')

DAO mapping

The appeal DAO needs to write the PHP file, and the frame here also provides a simple version of the mapping

User can configuration in /config/database.php, for example:

// database.php
return array(
    'dbConfig' => array(
        // Equivalent to creating the testDAO.php
        'test' => 'Biny_Test'
    )
);

Then you can use testDAO in Action、Service、Model

// testAction.php
namespace app\controller;
/**
* use property to make IDE know the $testDAO meaning
* @property \biny\lib\SingleDAO $testDAO
*/
class testAction extends baseAction
{
    public function action_index()
    {
        // The testDAO here is generated by the mapping without the cache operation in baseDAO
            [['id'=>1, 'name'=>'xx', 'type'=>2], ['id'=>2, 'name'=>'yy', 'type'=>3]]
        $data = $this->testDAO->query();
    }
}

Note: The mapped DAO does not have the function of setting database (master/slave database point to 'database')

And can not use function with PK cache (getByPK、updateByPK、deleteByPK etc)

If you need use function with PK cache, you should create dao file in /dao directory and configurate parameters you need

Simple query

DAO provided common query function like query, find, and pretty simple to use

// testAction.php
namespace app\controller;
/**
 * use property to make IDE know the $testDAO meaning
 * @property \app\dao\testDAO $testDAO
 */
class testAction extends baseAction
{
    public function action_index()
    {
        // return array with all data in testDAO, The format is two-dimensional array
            [['id'=>1, 'name'=>'xx', 'type'=>2], ['id'=>2, 'name'=>'yy', 'type'=>3]]
        $data = $this->testDAO->query();
        // first parameter is key you need to return [['id'=>1, 'name'=>'xx'], ['id'=>2, 'name'=>'yy']]
        $data = $this->testDAO->query(array('id', 'name'));
        // the second parameter is the dictionary key, will duplicate removal [1 => ['id'=>1, 'name'=>'xx'], 2 => ['id'=>2, 'name'=>'yy']]
        $data = $this->testDAO->query(array('id', 'name'), 'id');

        // return first data, the format is array ['id'=>1, 'name'=>'xx', 'type'=>2]
        $data = $this->testDAO->find();
        // parameter is key you need to return ['name'=>'xx']
        $data = $this->testDAO->find('name');
    }
}

Biny also support count, max, sum, min, avg etc Basic operations, count with parameter will return the count by duplicate removal

// count(*) 
$count = $this->testDAO->count();
// count(distinct `name`) duplicate removal
$count = $this->testDAO->count('name');
// max(`id`)
$max = $this->testDAO->max('id');
// min(`id`)
$min = $this->testDAO->min('id');
// avg(`id`)
$avg = $this->testDAO->avg('id');
// sum(`id`)
$sum = $this->testDAO->sum('id');

All operations here are simple operations, if you need complex methods or multiple table operations, you should use method addition

Delete/Update

You can use method like update, delete, add etc

Method update to update data, return success (true) for failure(false), condition can reference chapter seletor after

// update `DATABASE`.`TABLE` set `name`='xxx', `type`=5
$result = $this->testDAO->update(array('name'=>'xxx', 'type'=>5));

Method delete to delete the data, return success (true) for failure(false), condition can reference chapter seletor after

// delete from `DATABASE`.`TABLE`
$result = $this->testDAO->delete();

Mehtod add to add the data, when insert success will return the increasing ID, when second parameter false will return success (true) for failure(false)

// insert into `DATABASE`.`TABLE` (`name`,`type`) values('test', 1)
$sets = array('name'=>'test', 'type'=>1);
// return true/false
$id = $this->testDAO->add($sets, false);

Biny also can return affected_rows, you can configuration in /config/config.php, and replace parameter returnAffectedRows to true

Method update also can use in common operations, for example:

// update `DATABASE`.`TABLE` set `type`=`type`+5
$result = $this->testDAO->update(['type'=>['+'=>5]]);
// update `DATABASE`.`TABLE` set `type`=`count`-`num`-4
$result = $this->testDAO->update(['type'=>['-'=>['count', 'num', 4]]]);
        

Method createOrUpdate to add data and update when duplication

// The first parameter is the insert array, the second parameter is the update parameter when the failure occurs, and update the first parameter when not pass the second
$sets = array('name'=>'test', 'type'=>1);
$result = $this->testDAO->createOrUpdate($sets);

Method addList can batch adding data, return success (true) for failure(false)

// parameter is two-dimensional array
$sets = array(
    array('name'=>'test1', 'type'=>1),
    array('name'=>'test2', 'type'=>2),
);
$result = $this->testDAO->addList($sets);

Join Table

The framework supports the multi join table model, and the DAO classes have join, leftJoin, and rightJoin methods.

The parameter is the connection relation

// on `user`.`projectId` = `project`.`id` and `user`.`type` = `project`.`type`
$DAO = $this->userDAO->join($this->projectDAO, array('projectId'=>'id', 'type'=>'type'));

$DAO can continue to join, connect third tables, the connection is two-dimensional array, the first array corresponds to the first table and the new table relationship, the second array corresponds to second tables and the new table relationship

// on `user`.`testId` = `test`.`id` and `project`.`type` = `test`.`status`
$DAO = $DAO->leftJoin($this->testDAO, array(
    array('testId'=>'id'),
    array('type'=>'status')
));

You can continue to connect, the connection is the same two-dimensional array, three objects corresponding to the original table and the new table, unrelated to empty, the last empty array can be omitted

// on `project`.`message` = `message`.`name`
$DAO = $DAO->rightJoin($this->messageDAO, array(
    array(),
    array('message'=>'name'),
//  array()
));

And so on, in theory, any number of association tables can be established

There are two ways to write the parameters. The upper is the position corresponding table, and the other can be corresponding to the alias. The alias is the string before DAO

// on `project`.`message` = `message`.`name` and `user`.`mId` = `message`.`id`
$DAO = $DAO->rightJoin($this->messageDAO, array(
    'project' => array('message'=>'name'),
    'user' => array('mId'=>'id'),
));

Multiple tables can also use method query, find, count and other query statements. The parameter is changed into two dimensional array.

The same as the table parameter, there are two ways to write the parameters, one is the position corresponding table, the other is the alias corresponding table, and the same can also be mixed use.

// SELECT `user`.`id` AS 'uId', `user`.`cash`, `project`.`createTime` FROM ...
$this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->query(array(
      array('id'=>'uId', 'cash'),
      'project' => array('createTime'),
    ));

The join table conditions sometimes need to be equal to the fixed value, and can be added by the on method

// ... on `user`.`projectId` = `project`.`id` and `user`.`type` = 10 and `project`.`cash` > 100
$this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->on(array(
        array('type'=>10),
        array('cash'=>array('>', 100)),
    ))->query();

Multi table query and modification (update), and single table operation is basically the same, we need to pay attention to the single table parameters for one-dimensional array, multi table is a two-dimensional array, wrong will lead to execution failure.

Seletor

The DAO class can call filter (and selector), merge (or selector), and the effect is equivalent to filtering the data inside the table

The same selector supports single table and multi table operations, in which the single table is a one-dimensional array, and the multi table is a two-dimensional array

// ... WHERE `user`.`id` = 1 AND `user`.`type` = 'admin'
$filter = $this->userDAO->filter(array('id'=>1, 'type'=>'admin'));

The merge (or selector) is used to select the condition, and the condition is connected by or

// ... WHERE `user`.`id` = 1 OR `user`.`type` = 'admin'
$merge = $this->userDAO->merge(array('id'=>1, 'type'=>'admin'));

Similarly, multiple table parameters can also be used with the alias table, and the usage is consistent with the above

// ... WHERE `user`.`id` = 1 AND `project`.`type` = 'outer'
$filter = $this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(
        array('id'=>1),
        array('type'=>'outer'),
    ));

The $filter condition can continue to call the filter/merge method, and the condition will continue to filter on the original basis

// ... WHERE (...) OR (`user`.`name` = 'test')
$filter = $filter->merge(array('name'=>'test');

$filter can also be used as parameters to the filter/merge method. The effect is conditional superposition.

// ... WHERE (`user`.`id` = 1 AND `user`.`type` = 'admin') OR (`user`.`id` = 2 AND `user`.`type` = 'user')
$filter1 = $this->userDAO->filter(array('id'=>1, 'type'=>'admin');
$filter2 = $this->userDAO->filter(array('id'=>2, 'type'=>'user'));
$merge = $filter1->merge($filter2);

The DAO of the condition itself must be consistent with the DAO of the selected object, whether it is a and selector or a or selector, and the condition itself as a parameter. Otherwise, the exception will throw an exception

It is worth noting that the order of filter and merge has an impact on conditional screening

for example:

// WHERE (`user`.`id`=1 AND `user`.`type`='admin') OR `user`.`id`=2
$this->userDAO->filter(array('id'=>1, 'type'=>'admin')->merge(array('id'=>2));

// WHERE `user`.`id`=2 AND (`user`.`id`=1 AND `user`.`type`='admin')
$this->userDAO->merge(array('id'=>2))->filter(array('id'=>1, 'type'=>'admin');

As you can see from the above example, the correlation between the additions is consistent with the following selector expressions

Selector is the same way to get data as DAO, single table selector has all methods like query, update, delete etc. and multi table has methods like query, update etc

// UPDATE `DATABASE`.`TABLE` AS `user` SET `user`.`name` = 'test' WHERE `user`.`id` = 1
$result = $this->userDAO->filter(array('id'=>1)->update(array('name'=>'test'));

// SELECT * FROM ... WHERE `project`.`type` = 'admin'
$result = $this->userDAO->join($this->projectDAO, array('projectId'=>'id'))
    ->filter(array(array(),array('type'=>'admin')))
    ->query();

Whether it is filter or merge, before the execution of the SQL statement will not be executed, will not increase the burden of SQL, you can rest assured to use.

Complex select

In addition to the normal match selection, other complex selectors are also provided in filter and merge.

If in array the value is a array, will automatically become the in statement

// WHERE `user`.`type` IN (1,2,3,'test')
$this->userDAO->filter(array('id'=>array(1,2,3,'test')));

Others include >,<,>=,<=,!=,<>,is,is not, similarly, multi table cases with two-dimensional array to package

// WHERE `user`.`id` >= 10 AND `user`.`time` >= 1461584562 AND `user`.`type` is not null
$filter = $this->userDAO->filter(array(
    '>='=>array('id'=>10, 'time'=>1461584562),
    'is not'=>array('type'=>NULL),
));

// WHERE `user`.`id` != 3 AND `user`.`id` != 4 AND `user`.`id` != 5
$filter = $this->userDAO->filter(array(
    '!='=>array('id'=>array(3, 4, 5))
));

In addition, the like statement is also supported, which matches the beginning and end of the regular symbol:

// WHERE `user`.`name` LIKE '%test%' OR `user`.`type` LIKE 'admin%' OR `user`.`type` LIKE '%admin'
$filter = $this->userDAO->merge(array(
    '__like__'=>array('name'=>'test', 'type'=>'^admin', 'type'=>'admin$'),
));

// WHERE `user`.`name` LIKE '%test%' OR `user`.`name` LIKE 'admin%' OR `user`.`name` LIKE '%demo'
$filter = $this->userDAO->merge(array(
    '__like__'=>array(
        'name'=>array('test', '^admin', 'demo$'),
    )
));

not in grammar is temporarily not support, can use multiple != or <> instead.

At the same time, filter/merge can also be called iteratively to cope with complex queries with uncertain filtering conditions

// Action file
$DAO = $this->userDAO;
if ($status=$this->param('status')){
    $DAO = $DAO->filter(array('status'=>$status));
}
if ($startTime=$this->param('start', 0)){
    $DAO = $DAO->filter(array('>='=>array('start'=>$startTime)));
}
if ($endTime=$this->param('end', time())){
    $DAO = $DAO->filter(array('<'=>array('end'=>$endTime)));
}
// get count by selector
$count = $DAO->count();
// Get the first 10 data of composite condition
$data = $DAO->limit(10)->query();

other conditions

The conditional method can be called in DAO or selector, and the method can be called by transfer. The conditions in the same method are automatically merged

These include group,addition,order,limit,having

// SELECT `user`.`id`, avg(`user`.`cash`) AS 'a_c' FROM `TABLE` `user` WHERE ...
        GROUP BY `user`.`id`,`user`.`type` HAVING `a_c` >= 1000 ORDER BY `a_c` DESC, `id` ASC LIMIT 20,10;
$this->userDAO //->filter(...)
    ->addition(array('avg'=>array('cash'=>'a_c'))
    ->group(array('id', 'type'))
    ->having(array('>='=>array('a_c', 1000)))
    ->order(array('a_c'=>'DESC', 'id'=>'ASC'))
    // limit The first parameter is the number of entries, and the second parameter is the starting position (default is 0)
    ->limit(10, 20)
    ->query(array('id'));

addition is a method of data processing, providing max,count,sum,min,avg and other computing methods

two dimensional array is also required for multiple tables

// SELECT avg(`user`.`cash`) AS 'a_c', avg(`user`.`time`) AS 'time',
        sum(`user`.`cash`) AS 'total', min(`test`.`testid`) AS 'testid'
        FROM `TABLE1` `user` join `TABLE2` `test` ON `user`.`id` = `test`.`user_id` WHERE ...
        GROUP BY `user`.`id`,`user`.`type` HAVING `a_c` >= 1000 ORDER BY `a_c` DESC, `id` ASC LIMIT 0,10;
$DAO = $this->userDAO->join($this->testDAO, array('id'=>'user_id'))
$DAO //->filter(...)
    ->addition(array(
        array(
            'avg'=>array('cash'=>'a_c', 'time'),
            'sum'=>array('cash'=>'total'),
        ),
        array(
            'min'=>array('testid'),
        ),
    )->query();

Each condition is independent, and does not affect the original DAO or selector, you can use it safely

// This object is not changed by adding conditions
$filter = $this->userDAO->filter(array('id'=>array(1,2,3,'test')));
// 2
$count = $filter->limit(2)->count()
// 4
$count = $filter->count()
// 100 (total number of rows in user table)
$count = $this->userDAO->count()

SQL template

The framework provides the selector, conditional statements, contingency table, basically covering all the SQL syntax, but there may be some uncommon usage cannot be achieved, so it provides a way to use SQL templates, support for user-defined SQL statement, but not recommended for the user, if you must use it. Please be sure to do a good job in their own anti SQL injection

There are two ways to use,select (query return data), and command (execute return bool or affected rows)

It will automatically replace column :where,:table,:order,:group,:addition

// select * from `DATABASE`.`TABLE` WHERE ...
$result = $this->userDAO->select('select * from :table WHERE ...;');

// update `DATABASE`.`TABLE` `user` set name = 'test' WHERE `user`.`id` = 10 AND type = 2
$result = $this->userDAO->filter(array('id'=>10))
    ->command('update :table set name = 'test' WHERE :where AND type = 2;')

// select id,sum(`cash`) as 'cash' from `DATABASE`.`TABLE` WHERE `id`>10
    GROUP BY `type` HAVING `cash`>=100 ORDER BY `id` desc;
$result = $this->userDAO->filter(array('>'=>array('id'=>10)))
    ->group(array('type'))->having(array('>='=>array('cash'=>100)))->order(array('id'=>'desc'))
    ->addition(array('sum'=>array('cash'=>'cash')))
    ->select('select id,:addition from :table WHERE :where :group :order;');

You can also add some custom variables that automatically SQL escape to prevent SQL injection

The key placeholder is ;, for example ;key, value placeholder is :, for example:value

// select `name` from `DATABASE`.`TABLE` WHERE `name`=2
$result = $this->userDAO->select('select ;key from :table WHERE ;key=:value;', array('key'=>'name', 'value'=>2));

At the same time, the replacement content can also be an array, and the system will automatically replace the string to be connected with ,

// select `id`,`name` from `DATABASE`.`TABLE` WHERE `name` in (1,2,3,'test')
$result = $this->userDAO->select('select ;fields from :table WHERE ;key in (:value);',
    array('key'=>'name', 'value'=>array(1,2,3,'test'), 'fields'=>array('id', 'name')));

The above replacement methods will be SQL escape, it is recommended that users use template replacement, rather than their own variables into the SQL statement, to prevent SQL injection

Cursor data

If the data taken out of DB is very large, and PHP cannot afford such a large amount of memory to process it, the cursor method is needed at this time

The cursor can take out the data of the complex condition one by one and batch processing in the program, so as to reduce the memory bottleneck caused by the big data

// Selector, the conditional class schema is exactly the same, using the cursor method when the data is acquired
$rs = $this->testDAO->filter(array('type'=>1))->cursor(array('id', 'name'));
//Take out data one by one by Database::step ,e.g: ['id'=>2, 'name'=>'test']
while ($data=Database::step($rs)){
    do something...
}

If you use the SQL template, you can also pass the third parameter Database::FETCH_TYPE_CURSOR to achieve the use of cursors

// the same to above
$rs = $this->testDAO->filter(array('type'=>1))
  ->select('SELECT * FROM :table WHERE :where AND status=:status', array('status'=>2), Database::FETCH_TYPE_CURSOR);
// Take out data one by one by Database::step, e.g: ['id'=>2, 'name'=>'test', 'type'=>1, 'status'=>2]
while ($data=Database::step($rs)){
    do something...
}

SQL transaction

The framework provides a simple transaction processing mechanism for DAO, which is closed by default and can be opened by Datebase::start() method

Note: make sure that the linked data table is the storage engine of innodb, and that the transaction does not work.

After Datebase::start(), you can commit and save the entire transaction by Datebase::commit(), but it doesn't affect the operation before start

Similarly, you can roll back the entire transaction through Datebase::rollback() and rollback all the uncommitted transactions

When the program calls Datebase::end() method, the transaction will all terminate, the uncommitted transaction will be automatically rolled back; in addition, when the program is structured, it will automatically roll back the uncommitted transaction

// before the start of transaction will be submitted, num:0
$this->testDAO->filter(['id'=>1])->update(['num'=>0]);
// start transaction
Database::start();
// set num = num+2
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
// rollback transaction
Database::rollback();
// the num is still 0
$num = $this->testDAO->filter(['id'=>1])->find()['num'];
// set num = num+2
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
$this->testDAO->filter(['id'=>1])->update(['num'=>['+'=>1]]);
// commit transaction
Database::commit();
// num = 2
$num = $this->testDAO->filter(['id'=>1])->find()['num'];
// close transaction
Database::end();

In addition, transaction opening does not affect the select operation, only to increase, delete, modify operations have an impact

Data cache

The frame here for PK key index data can be cached by inheriting baseDAO, default is closed, and define $_pkCache = true in DAO to open

Then you need to make table key values in DAO, and compound indexes need to pass array, such as ['id','type']

Because the system cache defaults to redis, it is necessary to configure the corresponding redis configuration of the /app/config/dns_xxx.php

// testDAO
namespace app\dao;
class testDAO extends baseDAO
{
    protected $dbConfig = ['database', 'slaveDb'];
    protected $table = 'Biny_Test';
    // table primary key, double keys such as ['id', 'type']
    protected $_pk = 'id';
    // enable pk cache
    protected $_pkCache = true;
}

baseDAO中提供了getByPk,updateByPk,deleteByPk方法,

baseDAO provides getByPk,updateByPk,deleteByPk methods, when the $_pkCache parameter is true, the data will go cache, speed up the data reading speed.

getByPk Read the PK data, return one-dimensional array data

// parameter is PK value, return ['id'=>10, 'name'=>'test', 'time'=>1461845038]
$data = $this->testDAO->getByPk(10);

// Compound PK needs to pass array
$data = $this->userDAO->getByPk(array(10, 'test'));

updateByPk update row by PK

// parameters is PK value, update array and return true/false
$result = $this->testDAO->updateByPk(10, array('name'=>'test'));

deleteByPk delete row by PK

// parameters is PK value, return true/false
$result = $this->testDAO->deleteByPk(10);

Note: open $_pkCache DAO does not allow update and delete methods to be used again, which can lead to a phenomenon of cache and data asynchrony.

If the table frequently deletion data, the proposed closure of the $_pkCache parameter, or in the data deletion and then call clearCache() method to clear the cache, so as to keep pace with the contents of the database.

SQL debugging

SQL debugging method has been integrated in the framework event, only need to debug the statement before the method called Event::on(onSql), you can output the SQL statement in the page console

// The one method is bound to an incident, automatic release after one output
Event::one(onSql);
$data = $this->testDAO->query();

// The on method binds the event until the off method is called
Event::on(onSql);
$data = $this->testDAO->query();
$data = $this->testDAO->query();
$data = $this->testDAO->query();
Event::off(onSql);

The SQL event function can also be bound by itself, and the specific usage will be expanded in detail later in the event

Page render

Please open short_open_tag in php.ini configuration and use simplified template to improve development efficiency

The view directory of the page is under /app/template/, and can be returned in the Action layer by the $this->display() method

The general Action class extends the baseAction class. In baseAction, some common page parameters can be issued together to reduce development and maintenance costs

Param render

The display method has three parameters, the first one is the specified template file, the second is the page parameter array, and the third is the system class data (default is empty array).

// return /app/template/main/test.tpl.php 
return $this->display('main/test', array('test'=>1), array('path'=>'/test.png'));

/* /app/template/main/test.tpl.php
return:
<div class="container">
    <span> 1  </span>
    <img src="/test.png"/>
</div> */
<div class="container">
    <span> <?=$PRM['test']?>  </span>
    <img src="<?=$path?>"/>
</div>

The data of the second parameters will be placed in the $PRM page object. The third parameter is rendered directly, which is suitable for static resource addresses or class data

Custom TDK

The page TKD is generally defined by default in common.tpl.php. If the page needs to modify the corresponding title,keywords,description, it can be assigned after the Response is generated

$view = $this->display('main/test', $params);
$view->title = 'Biny';
$view->keywords = 'biny,php,framework';
$view->description = 'Biny is a tiny, high-performance PHP framework for web applications';
return $view;

Anti-XSS

Using the framework display method, the parameter HTML instantiation is automatically implemented to prevent XSS injection.

There are two ways to get the parameters in $PRM. The general array content is obtained, and it will be transferred automatically

// display <div>, Source code is &lt;div&gt;
<span> <?=$PRM['test']?>  </span>

In addition, you can use the private parameters to obtain, but will not be escaped, suitable for the need to display the full page structure requirements (ordinary pages are not recommended to use, the hidden danger is great)

// display <div>, Source code is <div> 
<span> <?=$PRM->test?>  </span>
// Same effect
<span> <?=$PRM->get('test')?>  </span>

In a multi tier data structure, it can also be used recursively

// display <div>, source code is &lt;div&gt;
<span> <?=$PRM['array']['key1']?>  </span>
<span> <?=$PRM['array']->get(0)?>  </span>

The array parameters of multi layer structure will be automatically escaped when used, and will not be escaped when they are not used, so as to avoid waste of resources and affect rendering efficiency.

Note: the third parameter is HTML instantiation or not, can be configuration by the field objectEncode in /config/config.php.

Param function

In addition to rendering, render parameters also provide some original array methods, for example:

in_array

// equal in_array('value', $array)
<? if ($PRM['array']->in_array('value') {
    // do something
}?>

array_key_exists

// equal array_key_exists('key1', $array)
<? if ($PRM['array']->array_key_exists('key1') {
    // do something
}?>

Other methods and so on, the use of the same way, such as json_encode

// Assign parameters to JS, var jsParam = {'test':1, "demo": {"key": "test"}};
var jsParam = <?=$PRM['array']->json_encode()?>;

Determine whether the array parameter is empty, you can directly call $PRM['array']() or $PRM('array') method to judge, the effect is equivalent to !empty() method

// equals if (!empty($array))
<? if ($PRM'array')) {
    // do something
}?>

Other parameter methods can be defined by themselves in /lib/data/Array.php

For example, define a len method and return the length of the array

/lib/data/BinyArray.php
public function len()
{
    return count($this->storage);
}

and then can used in tpl

// Assign parameters to JS, var jsParam = 2;
var jsParam = <?=$PRM['array']->len()?>;

Event

The event mechanism is provided in the framework, which is convenient for global call. Among them, the system default has providedcode>beforeAction,afterAction,onException,onError,onSql these

beforeAction executed before the execution of Action (triggered after init() method)

afterAction executed after the execution of Action (triggered before rendering page)

onExceptionWhen the system throws an exception, it is triggered, and the error code is passed, and code is defined in /config/exception.php

onErrorWhen the program calls the $this->error($data) method, it is triggered to pass the $data parameter

onSqlThe execution of the statement is triggered, and the Event::on(onSql) in the above example uses this event

Defining events

The system provides two ways to define events, one is to define long events $fd = Event::on($event, [$class, $method]), and that it will take effect until off.

Parameters are event names, methods[class, method name] method can not pass, default is Logger::event() method, will print in page console

$fd returns the operator of the event. When invoking the off method, the event can be bound by passing the operator.

namespace app\controller;
/**
* main Action
* @property \app\service\testService $testService
*/  
class testAction extends baseAction
{
    // init
    public function init()
    {
        // To trigger the beforeAction event, it can be defined in init and will be triggered after init
        Event::on(beforeAction, array($this, 'test_event'));
    }

    // default route index
    public function action_index()
    {
        // Binding the my_event1 method and the my_event2 method in testService to the myEvent event, the two methods are executed and executed in the order of binding
        $fd1 = Event::on('myEvent', array($this->testService, 'my_event1'));
        $fd2 = Event::on('myEvent', array($this->testService, 'my_event2'));

        // do something ..... 

        // unbind method my_event1 in event myEvent
        Event::off('myEvent', $fd1);

        // unbind all events myEvent, all the myEvent events will not executed again
        Event::off('myEvent');

        return $this->error('for test');
    }

    // custom event
    public function test_event($event)
    {
        // addLog is the method to write log
        Logger::addLog('trigger beforeAction event');
    }
}

Another binding is a single binding event Event::one(), which calls the same parameter and returns the $fd operator, which is automatically bound when the event is triggered once

$fd = Event::one('myEvent', array($this, 'my_event'));

Of course, if you want to bind multiple but not long term bindings, the system also provides a bind method with similar parameter usage.

// The first parameter binding method, the second is the event name, the third is the number of bindings,
    and the trigger number is automatically released after the full number of times.
$fd = Event::bind(array($this, 'my_event'), 'myEvent', $times);

Trigger events

Users can customize events and trigger selectively, and can directly use Event::trigger($event, $params) method

There are two parameters, the first is the trigger event name, and the second is the trigger transfer parameter, which will be passed to the trigger method

// trigger myEvent event
Event::trigger('myEvent', array(get_class($this), 'test'))

// the method defined in bind event
public function my_event($event, $params)
{
    // array('testService', 'test')
    var_dump($params);
}

Form Validation

Biny provides a complete set of form validation solutions that apply to most scenarios.

Form validation supports all types of validation and custom methods

for example:

namespace app\form;
use biny\lib\Form;
/**
 * @property \app\service\testService $testService
 * A custom form validation class extends Form
 */
class testForm extends Form
{
    // Define form parameters, types, and default values (default null)
    protected $_rules = [
        // id must be int, default is 10
        'id'=>[self::typeInt, 10],
        // name most not empty (include null, empty string)
        'name'=>[self::typeNonEmpty],
        // Custom verification method (valid_testCmp)
        'status'=>['testCmp']
    ];

    // Custom verification method
    public function valid_testCmp()
    {
        // Can call Service and DAO like Action layer
        if ($this->testService->checkStatus($this->status)){
            // pass the validation
            return $this->correct();
        } else {
            // failure, the error message can get by getError method
            return $this->error('Illegal type');
        }
    }
}

After define the validation class, you can use it in Action, and you can load the form through the getForm method

// load testForm
$form = $this->getForm('test');
// verificate form column, true/false
if (!$form->check()){
    // get error message
    $error = $form->getError();
    return $this->error($error);
}
// Get form parameter
$status = $form->status;
// Get form all data, return as array ['id'=>1, 'name'=>'billge', 'status'=>2]
$data = $form->values();
        

Note: undefined fields in $_rules cannot be obtained in $form, and even without verification, it is best to define them

In many cases, the form parameters are not all the same, and the system supports Form reuse, that is, you can customize some of the content in the generic Form class

For example, testForm, the example above, has a similar form, but it has one field type, and it needs to change the way status is validated

You can add a method in testForm

// method in testForm
public function addType()
{
    // add type column, default 'default', rule is not empty
    $this->_rules['type'] = [self::typeNonEmpty,'default'];
    // Modify the status judgment condition, changed to valid_typeCmp() method validation, remember to write this method
    $this->_rules['status'][0] = 'typeCmp';
}

Then loading the form in Action also requires adding 'addType' as a parameter, and others use the same method

$form = $this->getForm('test', 'addType');

You can write multiple additional methods in a form validation class, and they don't have any impact on each other directly

Verification type

The system provides 7 default authentication methods. When the verification fails, the error information is recorded, and the user can obtain it through the getError method

self::typeInt Numeric types, including integer floating point types, negative numbers

self::typeBool Determine whether it is true/false

self::typeArray Determine whether the array type

self::typeObject Determine whether it is an object data

self::typeDate Determine whether it is a legitimate date

self::typeDatetime Determine whether it is a legitimate datetime

self::typeNonEmpty Determine whether the non empty (including null, empty string)

self::typeRequired With this parameter, it can be an empty string

The verification type covers almost all cases. If there is a type that cannot be satisfied, the user can customize the validation method, which is no longer explained in the above examples

Debug

There are two debugging methods in Biny, one is debugging in the page console, and the other is convenient for the user to debug the corresponding web page.

The other is debugging in the log, just like any other framework

Console Debug

A major feature of Biny is the way the console is debugged, users can debug the data they want, and it doesn't have an impact on the current page structure.

The debug switch is in /web/index.php

// Console debugging, after the closure of the console will not output content
defined('SYS_CONSOLE') or define('SYS_CONSOLE', true);

Synchronous and asynchronous can also debug, but asynchronous debugging is the need to refer to the /static/js/main.js file, so asynchronous Ajax request will also debug information output in the console.

Debugging method is very simple, the global can call Logger::info($message, $key), in addition to warn, error, log and so on

The first parameter is the content that you want to debug, and also supports the array, the output of the Object class. The second parameter is debugging key, default is phpLogs

Logger::info() info debug

Logger::warn() warning debug

Logger::error() error debug

Logger::log() log debug

Here's a simple example, and the output of the console. The results will be different because browsers are different, and the effect is the same.

// can use anywhere in framework
Logger::log(array('cc'=>'dd'));
Logger::error('this is a error');
Logger::info(array(1,2,3,4,5));
Logger::warn("ss", "warnKey");

In addition, the Logger debug class also supports the output of time and memory, which can be used to optimize the performance of the code.

// At the beginning of the end, with time and memory, you can get the performance of the intermediate program
Logger::time('start-time');
Logger::memory('start-memory');
Logger::log('do something');
Logger::time('end-time');
Logger::memory('end-memory');

Log debug

The log directory of the platform is in /logs/. Please make sure that the directory has write permissions

The exception record will be generated in the error_{date}.log file, such as: error_2016-05-05.log

Debug records will be generated in the log_{date}.log file, such as: log_2016-05-05.log

In the program, you can add a log by calling Logger::addLog($log, INFO), and Logger::addError($log, ERROR) adds the exception

$log parameter supports the array and automatically prints the array

$LEVEL can use constants (INFODEBUGNOTICEWARNINGERROR) default is level programe given.

The system program errors will also be displayed in the error log. If the page appears 500, you can see the location in the error log

Shell execution

In addition to providing HTTP request processing, the Biny framework also provides a complete set of script execution logic

The execution entry is the shell.php file in the root directory, and the user can execute by php shell.php {router} {param} call through the command line

router is script routing, param is execution parameter, can default or multiple parameters

// shell.php
// set timezone
date_default_timezone_set('Asia/Shanghai');
// enabled shell model (in shell.php is true)
defined('RUN_SHELL') or define('RUN_SHELL', true);
// dev pre pub environment
defined('SYS_ENV') or define('SYS_ENV', 'dev');

Shell route

The routing is basically consistent with the HTTP request pattern, which is divided into {module}/{method} forms, in which {method} can be default and default is index

For example, index/test executes the action_test method in indexShell, and demo executes the action_index method in demoShell

If router defaults, the default reads the /config/config.php content in the router as the default route

// /config/config.php
return array(
    'router' => array(
        // http default route
        'base_action' => 'demo',
        // shell default route
        'base_shell' => 'index'
    )
)
// /app/shell/indexShell.php
namespace app\shell;
use biny\lib\Shell;
class testShell extends Shell
{
    // Like HTTP, the init method is executed first
    public function init()
    {
        //return 0 or not returned, the program continues to execute.
        If the other content is returned, the program terminates after outputting the content.
        return 0;
    }

    //default route index
    public function action_index()
    {
        // Returns an exception and logs and outputs it at the terminal
        return $this->error('execute error');
    }
}

Shell Param

The script executes the arguments that can pass the complex number, and the HTTP request can be caught directly in the method. The order is consistent with the parameter order, and can be default

In addition, the param method can be used to obtain the parameters of the corresponding position

For example, the terminal executes php shell.php test/demo 1 2 aaa, and the results are as follows:

// php shell.php test/demo 1 2 aaa
namespace app\shell;
use biny\lib\Shell;
class testShell extends Shell
{
    test/demo => testShell/action_demo
    public function action_demo($prm1, $prm2, $prm3, $prm4='default')
    {
        //1, 2, aaa, default
        echo "$prm1, $prm2, $prm3, $prm4";
        //1
        echo $this->param(0);
        //2
        echo $this->param(1);
        //aaa
        echo $this->param(2);
        //default
        echo $this->param(3, 'default');
    }
}

At the same time, the framework also provides variable parameter delivery, which is consistent with the HTTP schema

For example, the terminal executes the php shell.php test/demo --name="test" --id=23 demo, and the results are as follows:

// php shell.php test/demo --name="test" --id=23 demo
namespace app\shell;
use biny\lib\Shell;
class testShell extends Shell
{
    test/demo => testShell/action_demo
    public function action_demo($id, $name='demo', $prm='default')
    {
        //23, test, default
        echo "$id, $name, $prm";
        //23
        echo $this->param('id');
        //demo
        echo $this->param('name');
        //default
        echo $this->param('prm', 'default');

        // Variables without arguments begin in order from zeroth bits
        // demo
        echo $this->param(0);
    }
}

Note: the use of variable transmission method, the default parameters will not capture the non parameter variables, such as the above demo needs to get through param method

Shell log

Script execution no longer has other functions of HTTP mode, such as form validation, page rendering, browser console debugging. So in Logger debugging class, info/error/debug/warning these methods will be changed to the terminal output

And can also call Logger::addLog and Logger::addError to write log operations

The log directory is saved in the /logs/shell/ directory. Please ensure that the directory has write permissions. The format is consistent with the HTTP pattern

Note: when the program returns to $this->error($msg), the system will default to call Logger::addError($msg), please do not repeat the call.

Others

Many single instances of the system can be directly obtained by App::$base

App::$base->request is the current request, access to the current address, client IP, etc.

App::$base->cache is request static cache, valid only in the current request

App::$base->session is system session, can be directly obtained and copied, set the expiration time

App::$base->memcache is system Memcache, can be directly obtained and copied, set the expiration time

App::$base->redis is system redis, can be directly obtained and copied, set the expiration time

Request

After entering the Controller layer, Request can be called. Here are a few common operations

// for example with /test/demo/?id=10 

// Get Action name, return test
App::$base->request->getModule();

// Get Action class, return testAction
App::$base->request->getModule(true);

// Get Method name return action_demo
App::$base->request->getMethod();

// Get Method name without 'action' return demo
App::$base->request->getMethod(true);

// Is asynchronous request or not returnfalse
App::$base->request->isAjax();

// Return relative path,  /test/demo/
App::$base->request->getBaseUrl();

// Return absolute path, http://www.billge.cc/test/demo/
App::$base->request->getBaseUrl(true);

// Return url with querys,  /test/demo/?id=10
App::$base->request->getUrl();

// Return url refer (last page url)
App::$base->request->getReferrer();

// Get page user agent
App::$base->request->getUserAgent();

// Get client ip
App::$base->request->getUserIP();

Cache

Biny provides a global cache in the lifetime of the program, which is very simple to use

// Only need to assign, can realize the cache setting
App::$base->cache->testkey = 'test';
// The acquisition takes the element directly, and null returns if it does not exist
$testKey = App::$base->cache->testkey;

At the same time, Cache also supports isset judgment and unset operation

// return true/false
$bool = isset(App::$base->cache->testKey);
// delete cache
unset(App::$base->cache->testKey);
        

Session

The setting and acquisition of session are relatively simple (same as cache). The object will not be created without calling the session to avoid the loss of performance.

// Only need to assign, can realize the session setting
App::$base->session->testkey = 'test';
// The acquisition takes the element directly, and null returns if it does not exist
$testKey = App::$base->session->testkey;

At the same time, the session can be closed by the method close() to avoid the deadlock problem of session

// Will reopen session to getting data again after close
App::$base->session->close();

The clear() method empties the contents of the current session

// After clear, it is null 
App::$base->session->clear();

At the same time, session also supports isset judgment

// return true/false
$bool = isset(App::$base->session->testKey);

The acquisition and setting of cookie are completed in App::$base->request, respectively, providing getCookie and setCookie methods

getCookie parameter is the cookie key value that needs, if not passed, then return all cookie, return with array structure

$param = App::$base->request->getCookie('param');

setCookieThere are 4 parameters, which are key, value, expiration time (unit seconds), path belonging to the cookie, the expiration date default is 1 days, the path default is '/'

App::$base->response->setCookie('param', 'test', 86400, '/');