PHP library for solving common web application development problems:
code organization, routing and templating.

The Goal

There are literaly thousands of frameworks out there for PHP and other languages. Most of them are inspired by either the powerful structure of Rails or by the simplicity of Sinatra.

The goal of this project was to create a standalone library that is just under 500 lines of source code and allows you to manage core areas of web-app development (code organization, routing and templating) in both the Rails and Sinatra way.

How you'll fetch records from your database or what ORM you will use, we leave up to you.

Setup

After you download marley and configure your web server to send all requests to the index.php, you're ready to start jammin'!

require_once 'marley.php';

$app = new Marley();

Marley settings

Of course, you will be able to easily change Marley's settings to match your needs. The default settings are:

$app->config([
    'base_url'                => null, // Marley figures it out.
    'root_dir'                => $_SERVER['DOCUMENT_ROOT'],
    'templates_dir'           => '/views',
    'layouts_dir'             => '/views/layouts',
    'layout'                  => 'main',
    'extension'               => '.html.php',
    'controller_dir'          => '/controllers',
    'controller_file_suffix'  => '_controller.php',
    'controller_class_suffix' => 'Controller'
]);

The meaning of each of these settings will become clear in later sections.

Sinatra Flavored Syntax

Let's start with a Sinatra-style syntax. Along the way, we will explore Marley's two core features:
routing with parameters and templating.

/app
  /views
    index.html.php
  index.php

The code below will match the "root" request, execute the callback function and render a template inside the "/views" directory called "index.html.php"

# index.php

$app->get('/', function() {
    $this->data->name = 'Bob';
    $this->render('index');
});

Every property you assign to $this->data object inside a callback function becomes a variable inside a template. By default, Marley assumes that all templates are located inside the "/views" directory.

<!-- /views/index.html.php -->

<div class="container">
    <h2>The name is: <?=$name?></h2>
</div>

The resulting rendered markup will look like this:

<div class="container">
    <h2>The name is: Bob</h2>
</div>

Parameters (dynamic parts of a route)

Parameters in a route start with a colon `:` symbol.
The values of each route parameter are then passed to a callback function as arguments.

The example below will match requests in the following way:
/artist/marley/45
/artist/queen/46
/artist/coldplay/77

# index.php

$app->get('/artist/:name/:id', function($name, $id) {
    // make a database query or something to get artist's info.
    $this->data->artist = get_data_from_db_by($id);
    $this->data->name = $name;

    $this->render('artist');
});
<!-- /views/artist.html.php -->

<div class="artist-page">
    <h2><?=$name?></h2>
    <p><?=$artist['genre']?></p>
    <p><?=$artist['upcoming_tour']?></p>
    <p><?=$artist['number_of_albums']?></p>
    etc...
</div>

For convenience, you can also access values of each route parameter in the $_GET array.

$app->get('/artist/:name/:id', function() {
    $this->data->artist = get_data_from_db_by($_GET[':id']);
    $this->data->name = $_GET[':name'];

    $this->render('artist');
});

Parameters with custom regex

When you specify a route parameter, Marley by default uses the [^/]+ regex under the hood to match it.
If you want more control or a specific match, you can specify a custom regex after the parameter name.

The syntax is:
:parameter_name(regular expression)

$app->get('/artist/:name/:id([0-9]+)', function($name, $id) {
    // If this route matched, then $id is definitely a whole number.
    // btw, you don't have to render a template, you can just print something.
    print $name . ' and ' . $id;
});

Layouts

If you're doing any kind of real-world web application, you will need layouts - template containers which contain repeated sections of your application - so you will not have to specify your link and script tags or a site header on every page.

Layouts are like normal templates, except they contain a keyword {{yield}} which is replaced by the rendered template later.

By default, Marley assumes that layouts are located inside "/views/layouts" directory and the default layout is called "main.html.php".

So if you have a directory structure like this:

/app
  /views
    /layouts
      main.html.php
    index.html.php
  index.php
<!-- /views/layouts/main.html.php -->

<!DOCTYPE html>
<html>
<head>
    <title>App title</title>
</head>
<body>
    <div class="container">
        {{yield}}
    </div>
</body>
</html>
<!-- /views/index.html.php -->

<div class="welcome">
    <h2>Hello, <?=$name?>, do you wanna join us?</h2>
</div>

The code below will match requests like this:
/bob
/nicole
/mike

$app->get('/:name', function($name) {
    $this->data->name = ucfirst($name);
    $this->render('index');
});

And automatically render the specified 'index' view inside the main layout. Since rendering a template inside a layout is the most common behaviour, you won't have to explicitly specify it. Marley will do it automatically by default.

The resulting markup will look like this:

<!DOCTYPE html>
<html>
<head>
    <title>App title</title>
</head>
<body>
    <div class="container">
        <div class="welcome">
            <h2>Hello Bob, do you wanna join us?</h2>
        </div>
    </div>
</body>
</html>

Prevent automatic layouting

If you have a main layout and you don't want your template to be rendered inside that layout, you can pass an option like this:

$app->get('/:name', function($name) {
    $this->data->name = ucfirst($name);
    $this->render('index', ['layout' => false]);
});

The resulting markup on the same request will be:

<div class="welcome">
    <h2>Hello Bob, do you wanna join us?</h2>
</div>

Specifying a different layout

In many cases, just one main layout will not be enough, so you'll create different layouts for different purposes.

Specifying what layout the template should be rendered in is very simple. The code below will render the 'index' template inside a '/views/layouts/promo.html.php' layout instead of the main layout.

$app->get('/:name', function($name) {
    $this->data->name = ucfirst($name);
    $this->render('index', ['layout' => 'promo']);
});

What about POST requests?

Nothing, really, just use a different method name. Everything else works the same way.

$app->post('/track/submit', function() {
    print_r($_POST);
});

Rails Flavored Syntax

OK, You've come this far already. We have explored Marley's basic routing, layouting and templating functionality. Now it's time to dig into code organization.

/app
  /controllers
    artist_controller.php
  /views
    artist/
      index.html.php
      show.html.php
      albums.html.php
  index.php

Sinatra-style routing is cool and simple, but creating large web applications requires a lot of code to be executed when a specific route is matched and specifying all that code inside a single callback function doesn't seems useful.

That's where Rails way of doing things comes in. On the surface the routing part looks even more simple than Sinatra's style. Take a look:

$app->get('/', 'artist#index');
$app->get('/artist/show/:id', 'artists#show');
$app->get('/artist/album/:title([a-z]+)', 'artists#albums');

There are however many things happening under the hood.

The main change, you'll notice, is that, instead of a callback function we specify a string, which has two parts in it, separated by a hash symbol. The part before the hash symbol is a controller name, the part after the hash is a method name inside that controller.

So, all above requests will be routed to the controller (a PHP class) called ArtistController, which is located inside the "/controllers" directory.

By default, Marley assumes that all controllers are located inside the "/controllers" directory and each file has a suffix "_controller" after the name.

So the controller class inside artist_controller.php file will look like this:

class ArtistController {

    public function index() {

    }

    public function show($id) {
        // make a database query or something to get artist's info.
        $this->data->artist = get_data_from_db_by($id);
    }

    public function album($title) {
        $this->data->title = $title;
    }

}

Note that we're not specifying what template should be rendered when these methods are called.

Marley, by default, will render a template with the same name as method, from a template directory with the same name as the controller.

So, if we have the main layout and the album templates like this:

<!-- /views/layouts/main.html.php -->

<!DOCTYPE html>
<html>
<head>
    <title>App title</title>
</head>
<body>
    <div class="container">
        {{yield}}
    </div>
</body>
</html>
<!-- /views/artist/album.html.php -->

<div class="album-page">
    <h2>Title of the album is: <?=$title?></h2>
</div>

All these requests:
/artist/album/jammin
/artist/album/innuendo
/artist/album/ghost

Will be routed to album() method inside ArtistController class and /views/artist/album.html.php template will be rendered. The resulting markup will look like this:

<!DOCTYPE html>
<html>
<head>
    <title>App title</title>
</head>
<body>
    <div class="container">
        <div class="album-page">
            <h2>Title of the album is: Jammin</h2>
        </div>
    </div>
</body>
</html>

All automatic layouting functionality is the same as explained in the Sinatra section.

Resources Routes

A resource route is a shortcut that maps common routes to a related method in a controller. So this line:

$app->resource('albums');

Is equivalent to all of the following routes:

$app->get('/albums', 'albums#index');
$app->get('/albums/new', 'albums#new_');
$app->post('/albums/create', 'albums#create');
$app->get('/albums/:id/edit', 'albums#edit');
$app->post('/albums/:id/update', 'albums#update');
$app->post('/albums/:id/delete', 'albums#delete');
$app->get('/albums/:id', 'albums#show');

We use the name new_, because PHP doesn't allows new to be a function's name. During automatic template rendering, Marley will therefore look for a template file called /album/new_.html.php

Is Rails way better?

If you follow the naming conventions described above, you'll be writing much less code. Out of the box, different parts of the application will be automatically connected!

But, there's no definite answer. From a practical expierience, Rails way of routing and code organization is preferable for large appliations while Sinatra's way is more suitable for small, single page apps.

In the end, it's entirely your call, that's why Marley gives you a choice.

Rendering

In the previous sections, we explored some basic functionality of the $this->render() method. Let's now dig into other types of its usage.

Searching a template file

There are three different ways to specify template path to the render() method:

1. Relative to a controller directory inside the templates directory or relative to the templates directory.

$this->render('edit');

If you're using Sinatra-style routing, this will search for a file called /views/edit.html.php or if you're using Rails-style routing and controller is called "Album", this will search for a file called /views/album/edit.html.php

2. Relative to the templates directory.

$this->render('shared/item');

No matter what style of routing you are using, this will always search files relative to the templates directory, in this example it will search for a file called /views/shared/item.html.php

3. Absolute path.

$this->render('/var/www/views/crazy');

If the name starts with a slash, Marley assumes its an absolute path, so it will search for a file called /var/www/views/crazy.html.php

Partials

Partial templates are lightweight template snippets that you can use inside other templates.
By convention, partial template names should start with underscore `_` character.

So, for example, if we have a directory structure like this:

/app
  /views
    _list_item.html.php
    index.html.php
    artists.html.php
  index.php

Both, index and artists templates can use _list_item partial template to render something.

# index.php

$app->get('/', function($name, $id) {
    $this->data->site_title = 'Welcome to the Index page.';
    $this->data->artists = [
        ['name' => 'Coldplay', 'year' => 1977],
        ['name' => 'Queen', 'year' => 1946],
        ['name' => 'Marley', 'year' => 1945]
    ];
    $this->render('index');
});

$app->get('/artists', function($name, $id) {
    $this->data->artists = [
        ['name' => 'Coldplay', 'year' => 1977],
        ['name' => 'Queen', 'year' => 1946],
        ['name' => 'Marley', 'year' => 1945]
    ];
    $this->render('artist');
});
<!-- /views/_list_item.html.php -->

<li>
    <b><?=$name?></b>, Year: <?=$year?> 
</li>
<!-- /views/index.html.php -->

<div class="index-page">
    <h2><?=site_title?></h2>
    <ul>
        <? foreach ($artists as $artist) : ?>
            <? $this->render(['partial' => '_list_item', 'data' => $artist]); ?>
        <? endforeach; ?>
    </ul>
</div>
<!-- /views/artists.html.php -->

<div class="artists-list">
    <ul>
        <? foreach ($artists as $artist) : ?>
            <? $this->render(['partial' => '_list_item', 'data' => $artist]); ?>
        <? endforeach; ?>
    </ul>
</div>

The result of the `/` index route will be this:

<div class="index-page">
    <h2>Welcome to the Index page.</h2>
    <ul>
        <li><b>Coldplay</b>, Year: 1977</li>
        <li><b>Queen</b>, Year: 1946</li>
        <li><b>Marley</b>, Year: 1945</li>
    </ul>
</div>

And the result of the `/artists` route will be this:

<div class="artists-list">
    <ul>
        <li><b>Coldplay</b>, Year: 1977</li>
        <li><b>Queen</b>, Year: 1946</li>
        <li><b>Marley</b>, Year: 1945</li>
    </ul>
</div>

JSON

Render a JSON data with HTTP content-type application/json

$album = [
    "name" => "Innuendo", 
    "year" => 1991, 
    "artist" => "Queen",
    "tracks" => [
        "Innuendo",
        "I'm Going Slightly Mad",
        "Headlong",
        "I Can't Live with You",
        "Ride the Wild Wind",
        "All God's People",
        "These Are the Days of Our Lives",
        "Delilah",
        "Don't Try So Hard",
        "The Hitman",
        "Bijou",
        "The Show Must Go On"
    ]
];

$this->render(['json' => $album]);

Outputs:

{
    "name": "Innuendo",
    "year": 1991,
    "artist": "Queen",
    "tracks": [
        "Innuendo",
        "I'm Going Slightly Mad",
        "Headlong",
        "I Can't Live with You",
        "Ride the Wild Wind",
        "All God's People",
        "These Are the Days of Our Lives",
        "Delilah",
        "Don't Try So Hard",
        "The Hitman",
        "Bijou",
        "The Show Must Go On"
    ]
}

Javascript

Render an output with HTTP content-type text/javascript.

$app->get('/dynamic-javascript', function() {
    $this->render(['js' => 'var year = 1945; console.log(year);']);
});
<!-- inside some template file -->
<script src="/dynamic-javascript"></script>

Check-out the console :)

Plain text

Render an output with HTTP content-type text/plain.

$this->render(['plain' => 'Hello']);

HTTP response:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Hello

HTML

Render an output with HTTP content-type text/html.

$this->render(['html' => '

Hello

']);

HTTP response:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
<h1>Hello</h1>

Different HTTP status code

Render an output with HTTP content-type text/html and error code 404

$this->render(['html' => '

Hello

', 'status' => 404]);

HTTP response:

HTTP/1.1 404 Not Found
Content-Type: text/html; charset=UTF-8
<h1>Hello</h1>

Sharing

Often you'll need global objects, functions and variables to be accessible inside templates and controllers. Marley gives you a share() method that does exactly that.

So, for example, if we have a global database object that has a function get_user_info() and a user object that holds users data like is_authenticated, name, age and etc.

// Global objects
$db = new DatabaseHelper();
$user = new User();

// Marley
$app = new Marley();

// Share global obects to Marley's templates and controllers.
$app->share('db', $db);
$app->share('user', $user);

// Use shared objects inside a callback function.
// $this->db is the shared database object and
// $this->user is the shared user object.
$app->get('/profile', function() {
    if ($this->user->is_authenticated) {
        $this->data->info = $this->db->get_user_info();
        $this->render('profile');
    } else {
        $this->redirect('/auth/login');
    }
});

You can also initialize classes directly as shared objects, so the above code could be rewritten like this:

$app->share('db', new DatabaseHelper());
$app->share('user', new User());

Sharing functions is also possible:

$app->share('print_escaped', function($str) {
    print htmlentities($str, ENT_QUOTES, 'UTF-8');
});

$app->get('/artist/:name', function($name) {
    $this->print_escaped($name); 
});

And you can use shared objects and functions inside a template as well.

<!-- /views/profile.html.php -->

<? if ($this->user->is_authenticated) : ?>
    <h2>Hello: <?=$this->user->name?></h2>
    <span>You are <?=$this->user->age?> years old.</span>
<? endif; ?>

Web server configuration

Here are configuration snippets for common web servers to redirect "non-file" requests to index.php

# Apache .htaccess

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
# nginx

server {
    location / {
        try_files $uri /index.php;
    }
}