by IvanLuLyf

GitHub Readme.md


BunnyPHP is a lightweight PHP MVC Framework.

Latest Stable Version Total Downloads License

Code Size GitHub stars

PHP Gitter

English | 中文

Run on Repl.it Open in Gitpod Deploy to Heroku


Directory Structure

Project                 Root Dir
├─index.php             Entry File
├─api.php               Api Entry File
├─app                   Default Application Dir
│  ├─controller         Controller Dir
│  ├─model              Model Dir
│  ├─service            Service Dir
│  ├─filter             Filter Dir
├─cache                 Default FileCache Dir
├─config                Configure Dir
│  ├─config.php         Default Configure File
├─lang                  Language Package Dir
├─static                Static Files Dir
├─template              Template Files Dir
├─upload                Default FileStorage Dir


Using Composer

Create a project by template

composer create-project ivanlulyf/bunnyphp-app PROJECT_NAME --no-dev

Create a project by your own

Run the command to get BunnyPHP

composer require ivanlulyf/bunnyphp

Create file index.php with the following content.

define('APP_PATH', __DIR__ . '/');
define("IN_TWIMI_PHP", "True", TRUE);
require 'vendor/autoload.php';
(new BunnyPHP\BunnyPHP())->run();

Using Clone

Clone the repo to your project root

git clone https://github.com/IvanLuLyf/BunnyPHP.git

Create file index.php with the following content.

define('APP_PATH', __DIR__ . '/');
define("IN_TWIMI_PHP", "True", TRUE);
require APP_PATH . 'BunnyPHP/BunnyPHP.php';
(new BunnyPHP\BunnyPHP())->run();


  • PHP >= 7.0
  • Database : MySQL SQLite or PostgreSQL

Server Setting


Add following content to .htacess file.

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php


location / {
    try_files $uri $uri/ /index.php$is_args$args;



PHP Config File

return [
    "namespace" => "\\App",           // project namespace(optional, empty by default)
    "apps" => [                       // configure for sub-app  
        "[url path]" => [             // [url path] sub-app's access path
            "path" => "admin",        // path to sub-app's program(optional, if using composer and sub-app's namespace is not empty)
            "namespace" => "\\App"    // sub-app's namespace
    "db"=> [
        "type"=>"sqlite",             // sqlite mysql pgsql
        "host"=>"",                   // database host
        "port"=>"",                   // database port
        "username"=>"",               // database username
        "password"=>"",               // database password
        "database"=>"bunny.sqlite3",  // database name
        "prefix"=>"tp_",              // table prefix
    "storage" => [                    // storage config, optional
        "name" => "file",             // storage name,will load [name]Storage
    "cache" => [                      // cache config, optional
        "name" => "file",             // cache name,will load [name]Cache
    "logger" => [                     // logger config, optional
        "name" => "file",             // cache name,will load [name]Logger
        "filename" => "log.log",      // log file path,if using FileLogger
    "site_name"=>"Your Site Name",    // your site name
    "site_url"=>"YourDomain.com",     // your site domain
    "controller"=>"Index",            // default controller

JSON Config File

to use this you should prevent other from getting this file.

  "site_name":"Your Site Name",

Framework Error Code

Code Description 0 ok -1 network error -2 mod does not exist -3 action does not exist -4 template does not exist -5 template rendering error -6 database error -7 parameter cannot be empty -8 internal error


Every Model must extend Model


class MessageModel extends Model
    protected $_column = [
        'id' => ['integer', 'not null'],
        'message' => ['text', 'not null'],
        'from' => ['varchar(32)', 'not null']

    protected $_pk = ['id']; // Primary Key

    protected $_ai = 'id';   // Auto Increment
    protected $_uk = [['message','from']];  //Unique Key List

Use MessageModel::create() to generate a table

Use chained calls to fetch data

$messages = (new MessageModel())->where('from = :f',['f'=>$from])
    ->order('id desc')

Add data

$id = (new MessageModel())->add(['message'=>$message,'from'=>$from]);

Update data

$affect_rows = (new MessageModel())->where('from = :f',['f'=>$from])
    ->update(['message'=>'new message']);

Delete data

$affect_rows = (new MessageModel())->where('from = :f',['f'=>$from])->delete();

Table Join

join(ModelClass,[Join Condition(optianal)],[Table Field (optional)],[Join Method])

Join Condition Format

Format Type Sample Description String ['id',] JoinedTable.id=CurrentTable.id Array [['id','msg_id'],] JoinedTable.id=CurrentTable.msg_id Key-value Pair ['id'=>1] JoinedTable.id = 1


$hellos = $this->join(TestModel::class, [['id', 'msg_id']], ['message'])
    ->fetchAll(['content', 'id']);

Generated SQL(table prefix is tp_)

select tp_hello.content,tp_hello.id,tp_test.message from tp_hello left join tp_test on (tp_test.id=tp_hello.msg_id); 

Controller and Router

Every Controller must extend Controller


class MessageController extends Controller
    public function ac_init_cli()
        $this->assign('response', 'Table Created')->render();

    public function ac_list(MessageModel $model)
        $messages = $model->fetchAll();
        $this->assign('messages', $messages)->render('list.html');

    public function ac_message_get($id, MessageModel $model)
        $message = $model->getMessage($id);
        $this->assign('message', $message)->render('view.html');

    public function ac_message_post($message, MessageModel $model)
        $id = $model->addMessage($message);
        $this->redirect('test', 'message', ['id' => $id]);


Enter php cli [mod] [act] in the console. If there is [Mod]Controller, the request will be handle by this class.

For example, php cli message init will handle by MessageController.

If there is a function like ac_[act]_cli in the controller class, the request is processed by this function. If it does not exist, it will look for the function ac_[act] to handle it. If they do not exist, an error is reported.

For example, php cli message init will look for the ac_init_cli response first.


In the browser, the request /[mod]/[act] will be responded to by the function in [Mod]Controller.

In particular, if the request does not contain [act], the value of [act] is index.

If the function name of the specified request method like ac_[act]_[method] exists in the controller class, for example, ac_message_get, ac_message_post or ac_message_put, the request will be processed first by these functions.

If these functions do not exist, they will be handled by ac_[act].

If there is no function like ac_[act] in the controller class, but there has the other function, the request will be handled by the other function, and you can use $this->getAction() to get the contents of [act].

If they do not exist, an error will be reported.


API requests start with /api/, which is like /api/[mod]/[act].

It will be displayed in JSON format.


API requests start with /ajax/, which is like /ajax/[mod]/[act].

It will be displayed in JSON format.


ac_[act]_[method] > ac_[act] > other

Dependency Injection

The framework automatically injects parameters when calling the Controller's Action function.

For Example

public function ac_test(UserModel $userModel,string $name,int $id=1){


In this example, the $userModel variable will automatically get a new UserModel() instance. $name will get the value of $_REQUEST['name'], if $_REQUEST['name'] is not set and the default value is not set, then return ''.$id will get the value of $_REQUEST['id'], if not set, get the default value 1.

In particular, if the function parameter does not specify a variable type, the value of $_REQUEST is automatically obtained as a string type.

Variable output

For the variable to be output, you need to call assign($name,$value) or assignAll($dataArray). Then call render([HTML page]) , error() or renderTemplate([HTML template]) rendering results page.


The Controller's Action function supports the use of annotations.

@param annotation

If there is path(postion) or path(position,default). in the @param annotation, the parameter will get the ability to get the Path variable.

For example:

class TestController{
     * @param $name string path(0,Test)
     * @param $page integer path(1,1)
    public function ac_test($page, $name){

In the request /test/test/Bunny/2, the variable $name will get the value of path(0) 'Bunny', The variable $page will get the value of path(1) 2.

In the request /test/test/Bunny, the variable $name will get the value of path(0) 'Bunny', The variable $page will get the default value of path(1) 1.

In the request /test/test, the variable $name will get the default value of path(0) 'Test', The variable $page will get the default value of path(1) 1.

In particular, if there is a variable $_REQUEST['name'] and the value of the path variable exists, the final value is the value in $_REQUEST.

For example, request /test/test/Bunny?name=PHP, and the final $name gets the value 'PHP'.

@filter annotation

If the @filter annotation is defined in the function, the doFilter function of the corresponding filter is called first, and then the Controller's Action function is executed.

For example

class TestController{
     * @filter test
     * @filter hello
    public function ac_test(){

It will call TestFilter'sdoFilter function first.If the return value is Filter::NEXT then execute the next filter, in the example it is HelloFilter. If the function return value is Filter::STOP then stop Execute the remaining Filter and Action functions.