Commit 0836ad05 authored by Avris's avatar Avris Committed by Andrzej Prusinowski

init

parents
Pipeline #3713324 passed with stage
in 5 seconds
vendor/*
.idea/*
**/.DS_Store
tests:
script:
- composer install --dev
- ./test
## Picco
*Golfy PHP framework*
---
**Code golf** is a type of recreational computer programming competition in which participants strive to achieve
the shortest possible source code that implements a certain algorithm. <sup>[\[source\]](https://en.wikipedia.org/wiki/Code_golf)</sup>
**Picco** is a tiny PHP web framework that only takes **~2,5 kB** of space and has no dependencies on other libraries,
while still providing quite a lot of features, being extensible and reasonably easy to use.
### Features
- **Dependency Injection**
- **Event dispatcher**: system events `request`, `response` and `error`
- REST-ful two-way **routing** with parameter recognition
- Basic **error handling**
- **Views** in `.phtml` files
- **CLI tasks**
### Requirements
- Code should be **as short as possible, but:**
- It **shouldn't look golfed** from the outside, i.e. "nice" namespaces, classnames and public method names are required
- Compromise between readability and brevity: new lines should be used to separate significant blocks of code, but indentation is unwanted
- Picco is a Composer package with **no dependencies**
### Installation
Just [install Composer](https://getcomposer.org/download/) and run:
composer create-project avris/picco-project my_new_project
Then copy-paste `parameters.php.dist` to `parameters.php` and fill it with your database access data.
To create database schema and fill it with some dummy data, run:
bin/picco fixtures
Also, configure the directory for the runtime files:
chmod -R 777 run
You should be able to access the project
at `http://localhost/my_new_project/web/app_dev.php` (for development environment)
and `http://localhost/my_new_project/web` (for production environment).
### Usage
Since an example is worth a thousand words, Picco comes with a **starter project**,
which serves as a demo of what you can do with Picco and how to extend it
(with [RedBeanPHP ORM](http://www.redbeanphp.com/index.php), cache, logs, translations...).
Check out its code!
#### Routing
Class `App\Routing` should have a public static `get` method that returns and array of routes.
Its keys are route names (same as controller name and view name) and values are
[regular expressions](https://en.wikipedia.org/wiki/Regular_expression) that correspond to them.
'home' => '/',
'itemList' => '/item/list',
'itemShow' => '/item/(\d+)/show',
Assumeing this set of routes: URL `/` will run the `home` controller, `/item/list` -- the `itemList` controller,
and anything like `/item/1/show`, `/item/7/show`, `/item/666/show`, etc. -- the `itemShow` controller.
Anything other than that will throw a 404 exception.
To generate a route, fetch the router from the DI container and use `get` method, for instance:
$c->router->get('itemShow', [$item->id])
#### Controllers
Controllers are public methods of the `App\Controllers` class. Their first parameter is allways the DI container,
while all the rest are consecutive matches from the route. They should return an array of variables
that will be passed on to the view. For instance:
public function itemShow(Container $c, $id)
{
$item = $c->db->load('item', $id);
if (!$item->id) { throw new \Exception("Item $id not found", 404); }
return ['item' => $item];
}
#### Views
Views are `.phtml` files in the `/views` directory with a name corresponding to the route/controller name.
They have access to the router (`$r`) and the values returned by the controller
(in case of `views/itemShow.phtml` it's just `$item`). You can also render partials using `$this->render($name, $vars)`:
<?= $this->render('partial/head', get_defined_vars()); ?>
<h2><?= $item->name ?></h2>
<a href="<?= $r->get('itemDelete', [$item->id])?>" class="btn btn-danger"><?=$t['delete']?></a>
<?= $this->render('partial/foot', get_defined_vars()); ?>
#### Constants
There are 5 constants defined by Picco:
* **D** -- root directory of the project, useful for instance for loading configuration files, setting up filesystem cache, etc.
* **F** -- base of the URL (like `picco/web/app_dev.php`), useful for generating links to other pages, if you don't want to use the router.
* **B** -- base of the URL, but without front controller (like `picco/web/`), useful for including assets, eg. `<img src=<?=B?>logo.png`.
* **R** -- current route
* **E** -- current environment: `true` for dev/debug mode (`app_dev.php`), `false` for prod mode (`app.php`)
#### Dependency Injection Container
Atfer Picco sets the system services (`router`, `controllers`, `view`, `dispatcher`),
it runs `App\Services::get($c)'` method. In there you can define your services and parameters:
$c->foo = 'bar';
$c->sizeChecker = new SizeChecker();
$c->parameters = require D.'parameters.php';
$c->db = function($c) {
define('REDBEAN_MODEL_PREFIX','\\App\\Model\\');
$p = $c->parameters['db'];
$db = new \R;
$db->setup($p['dsn'], $p['user'], $p['pass']);
if (!E) { $db->freeze(); }
return $db;
};
If what you're setting is a callable, it will be resolved (eagerly, on retrieval) with the container as a parameter.
To retrieve a service/parameter, simply get it, like: `$c->db->findAll(...)`.
#### Event Dispatcher
To define an event listener:
$c->dispatcher->set('event_name', function(Container $c, $moreParameters) {
// do something...
});
To trigger it:
$c->dispatcher->trigger('event_name')
Listeners are triggered in the order they were defined. The default listeners for the system events
(`request`, `response` and `error`) go off **after** user defined ones.
If any listener in the chain returns anything, the chain isn't executed anymore.
#### Error handling
Picco handles errors and exceptions in the following way: if `E=true` (debug mode), the exception is re-thrown,
otherwise, the `error` controller (with the container and that exception as parameters) will we executed,
so that you can display a 404/500/whatever error page.
You can overwrite that default behaviour by listening to the `error` event.
#### CLI tasks
Similarly to controllers, there is an `App\Tasks` class, public methods of which are CLI tasks.
If you run, for instance, `bin/picco test foo bar`, it will execute `App\Tasks::test('foo', 'bar')`.
### Contributing
Picco's source code is available at [Gitlab](https://gitlab.com/Avris/Picco),
feel free to create a pull request, if you can make it shorter or better in any way.
Note: no testing frameworks were used, to run the testsuite go to the library directory and run `./test`;
### Copyright ###
* **Author:** Andrzej Prusinowski [(Avris.it)](https://avris.it)
* **Licence:** [MIT](https://opensource.org/licenses/MIT)
{
"name": "avris/picco",
"description": "Golfy PHP framework",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "avris",
"email": "andrzej@avris.it",
"homepage": "https://avris.it"
}
],
"require": {},
"autoload": {
"psr-4": { "Picco\\": ["src"] }
},
"bin": ["picco"]
}
#!/usr/bin/env php
<?php
for($i=0;$i<4;$i++)if(file_exists($l=($d=__DIR__.str_repeat('/..',$i)).'/vendor/autoload.php')){require$l;break;}
define('D',"$d/");define('AB',"$d/web");define('E',true);
$c=new Picco\Container;$c->dispatcher=new Picco\Dispatcher;
$a=$argv;array_shift($a);App\Services::get($c);
if(!is_callable($d=[new App\Tasks,$m=array_shift($a)?:'help']))throw new Exception("Undefined task $m");
call_user_func_array($d,array_merge([$c],$a));
\ No newline at end of file
<?php
namespace Picco;class App{
function __construct($e){session_start();$s=$_SERVER;define('D',dirname($s['SCRIPT_FILENAME']).'/../');$l=strlen($r=@$s['PATH_INFO']);$u=explode('?',$s['REQUEST_URI'],2)[0];define('F',$l?substr($u,0,-$l):$u);define('R',rtrim($r,'/')?:'/');define('B',preg_replace('#/[^/]+\.php$#','',F).'/');define('E',$e);}
function run(){$c=new Container();$c->router=new Router(\App\Routing::get());$c->view=new View;$q=$c->controllers=new \App\Controllers;$d=$c->dispatcher=new Dispatcher;\App\Services::get($c);
$t=function($f,$m)use($c,$q,$d){$r=(array)call_user_func_array([$q,$f],$m);return$d->trigger('response',[$c,&$r])?:$c->view->render($f,$r+['r'=>$c->router]);};
$d->set('error',function($c, $e)use($t){if(E)throw$e;return$t('error',[$c,$e]);});
set_error_handler(function($t,$m,$f,$l)use($c,$d){if(!error_reporting())return;echo$d->trigger('error',[$c,new\ErrorException("$m in $f line $l",500)]);die;},E_ALL);
try{list($f,$m)=$c->router->match(R);return$d->trigger('request',[$c,$f,$m])?:$t($f,[0=>$c]+$m);}catch(\Exception$e){return$d->trigger('error',[$c,$e]);}}}
\ No newline at end of file
<?php
namespace Picco;class Container{
protected $s=[],$r=[];
function __set($n,$v){$this->s[$n]=$v;}
function __get($n){return isset($this->r[$n])?$this->r[$n]:$this->r[$n]=$this->b($this->s[$n]);}
protected function b($s){return is_callable($s)?$s($this):$s;}}
\ No newline at end of file
<?php
namespace Picco;class Dispatcher{
protected $e;
function set($n,$c){$this->e[$n][]=$c;}
function trigger($n,$a){foreach((array)@$this->e[$n]as$e)if(null!==$r=call_user_func_array($e,$a))return$r;}}
\ No newline at end of file
<?php
namespace Picco;class Router{
protected $r;
function __construct($r){$this->r=$r;}
function match($u){foreach($this->r as$n=>$p)if(preg_match("#^$p$#",$u,$m))return[$n,$m];throw new\Exception("Route for $u not found",404);}
function get($c, $o=[]){return F.(rtrim(strtr(preg_replace_callback('#\([^(]*\)#',function()use($o){static$count=0;return@$o[$count++];},$this->r[$c]),['?'=>'']),'/')?:'/');}}
\ No newline at end of file
<?php
namespace Picco;class View{function render($v, $p=[]){unset($p['v']);extract($p);ob_start();require D."views/$v.phtml";return ob_get_clean();}}
\ No newline at end of file
#!/usr/bin/env php
<?php
for($i=0;$i<4;$i++){if(file_exists($l=($d=__DIR__.str_repeat('/..',$i)).'/vendor/autoload.php')){require$l;break;}}
/** @var Exception[] $errors */
$errors = [];
class TestException extends Exception{};
function a($ok) {
global $errors;
if ($ok) {
echo '.';
return;
}
echo 'F';
$source = debug_backtrace()[0];
throw $errors[] = new TestException(sprintf("FAIL: %s:%s", basename($source['file']), $source['line']));
}
foreach (glob(__DIR__ . '/tests/*Test.php') as $t) {
try {
require $t;
} catch (TestException $e) {
} catch (\Exception $e) {
echo 'E';
$errors[] = $e;
}
}
echo "\n\n";
if (!count($errors)) {
echo "ALL TESTS PASSED\n\n";
exit(0);
}
foreach ($errors as $error) {
echo $error->getMessage() . "\n";
}
echo "\n";
exit(1);
\ No newline at end of file
<?php
namespace App;
use Picco\Container;
use Picco\App;
class Mock { public static $routing; public static $services; }
class Routing { public static function get() { return Mock::$routing; } }
class Services { public static function get(Container $c) { $cb = Mock::$services; $cb($c); } }
class Controllers {
public function foo() { return ['foo' => 8, 'bar' => 69]; }
public function error(Container $c, \Exception $e) { return ['message' => $e->getMessage()]; }
}
$_SERVER = [
'SCRIPT_FILENAME' => __DIR__ . '/help/web/app_dev.php',
'PATH_INFO' => '/test/',
'REQUEST_URI' => '/base/app_dev.php/test/?foo=bar',
];
$a = new App(false);
a(session_id());
a(D === __DIR__ . '/help/web/../');
a(F === '/base/app_dev.php');
a(B === '/base/');
a(R === '/test');
a(E === false);
Mock::$routing = [ 'foo' => '/test' ];
Mock::$services = function(Container $c) { };
$expected = <<<HTML
<p>foo = 8</p>
<p>view = foo</p>
<p>vars = {"foo":8,"bar":69,"r":{}}</p>
<p>bar = 69</p>
<p>view = bar</p>
<p>vars = {"foo":8,"bar":69,"r":{}}</p>
HTML;
a($a->run() === $expected . "\n");
Mock::$routing = [];
Mock::$services = function(Container $c) { };
a($a->run() === "<p>message = Route for /test not found</p>\n");
Mock::$routing = [ 'foo' => '/test' ];
Mock::$services = function(Container $c) { $c->dispatcher->set('request', function() { return 'blocked'; }); };
a($a->run() === 'blocked');
Mock::$routing = [ 'foo' => '/test' ];
Mock::$services = function(Container $c) { $c->dispatcher->set('response', function(Container $c, &$vars) { $vars['foo'] = 'overwritten'; }); };
$expected = <<<HTML
<p>foo = overwritten</p>
<p>view = foo</p>
<p>vars = {"foo":"overwritten","bar":69,"r":{}}</p>
<p>bar = 69</p>
<p>view = bar</p>
<p>vars = {"foo":"overwritten","bar":69,"r":{}}</p>
HTML;
a($a->run() === $expected . "\n");
Mock::$routing = [];
Mock::$services = function(Container $c) { $c->dispatcher->set('error', function(Container $c, \Exception $e) { return $e->getMessage(); }); };
a($a->run() === "Route for /test not found");
<?php
class FooService {
public static $count = 0;
function __construct() {
self::$count++;
}
}
class BarService {
public $foo, $lorem;
function __construct($foo, $lorem) {
$this->foo = $foo;
$this->lorem = $lorem;
}
}
$c = new Picco\Container();
$c->lorem = 'ipsum';
a('ipsum' == $c->lorem);
$c->foo = function() { return new FooService(); };
a(FooService::$count === 0);
a($c->foo instanceof FooService);
a(FooService::$count === 1);
a($c->foo instanceof FooService);
a($c->foo === $c->foo);
a(FooService::$count === 1);
$c->anotherFoo = new FooService();
a(FooService::$count === 2);
a($c->anotherFoo instanceof FooService);
a($c->anotherFoo instanceof FooService);
a($c->anotherFoo === $c->anotherFoo);
a(FooService::$count === 2);
a($c->foo !== $c->anotherFoo);
$c->bar = function($c) { return new BarService($c->foo, $c->lorem); };
a($c->bar instanceof BarService);
a($c->bar->foo instanceof FooService);
a($c->bar->foo === $c->foo);
a($c->lorem == 'ipsum');
<?php
$d = new Picco\Dispatcher;
$out = 0;
$d->set('foo', function($a, $b) use (&$out) { $out += $a + $b; if ($a == 0) { return true; } });
$d->trigger('foo', [1,2]);
a(3 == $out);
$d->trigger('foo', [3,4]);
a(10 == $out);
$d->trigger('foo', [0,1]);
a(11 == $out);
$d->set('foo', function($a, $b) use (&$out) { $out += 2*($a + $b); });
$d->trigger('foo', [1,2]);
a(20 == $out); // current 11 + first listener 3 + second listener 6
$d->trigger('foo', [0,4]);
a(24 == $out); // current 20 + first listener 4, second listener not executed
<?php
$r = new Picco\Router([
'home' => '',
'itemList' => '/item/list',
'itemShow' => '/item/(\d+)/show/?(.*)',
'changeLocale' => '/locale/([A-Z]{2})'
]);
function assertRouteMatch($url, $controller, $params = []) {
global $r;
a([$controller, array_merge([$url], $params)] === $r->match($url));
}
assertRouteMatch('', 'home');
assertRouteMatch('/item/list', 'itemList');
assertRouteMatch('/item/12/show', 'itemShow', ['12', '']);
assertRouteMatch('/item/2/show', 'itemShow', ['2', '']);
assertRouteMatch('/item/7/show/abc', 'itemShow', ['7', 'abc']);
assertRouteMatch('/locale/DE', 'changeLocale', ['DE']);
function assertRouteNotFound($url) {
global $r;
try {
$r->match($url);
a(false);
} catch (Exception $e) {
a($e->getMessage() === "Route for $url not found");
a($e->getCode() === 404);
}
}
assertRouteNotFound('/foo');
assertRouteNotFound('/item//show');
assertRouteNotFound('/locale/pl');
a($r->get('home') == F.'/');
a($r->get('itemList') == F.'/item/list');
a($r->get('itemShow', [10]) == F.'/item/10/show');
a($r->get('itemShow', [10, 'foo']) == F.'/item/10/show/foo');
a($r->get('changeLocale', ['EN']) == F.'/locale/EN');
a(@$r->get('xxx', ['EN']) == F.'/');
\ No newline at end of file
<?php
$v = new Picco\View;
$expected = <<<HTML
<p>foo = 8</p>
<p>view = foo</p>
<p>vars = {"foo":8,"bar":69}</p>
<p>bar = 69</p>
<p>view = bar</p>
<p>vars = {"foo":8,"bar":69}</p>
HTML;
$actual = $v->render('foo', ['foo' => 8, 'bar' => 69]);
a($actual === $expected . "\n");
\ No newline at end of file
<p>bar = <?= $bar ?></p>
<p>view = <?= $v ?></p>
<p>vars = <?= json_encode($p) ?></p>
<p>message = <?= $message ?></p>
<p>foo = <?= $foo ?></p>
<p>view = <?= $v ?></p>
<p>vars = <?= json_encode($p) ?></p>
<?= $this->render('bar', get_defined_vars()) ?>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment