emsifa/block

PHP Native Template System

v2.4.0 2018-03-28 14:09 UTC

This package is auto-updated.

Last update: 2024-05-22 01:01:51 UTC


README

Build Status License

Block is PHP native template system inspired by Laravel Blade. Block is not template language, so block doesn't need to be compiled and cached like blade, twig, smarty, etc.

Requirements

Block requires PHP 5.5 or greater

Installation

With Composer

If your project using composer, you can install Block via composer by typing this command:

composer require "emsifa/block"

Without Composer

Block is single file library, so you can easily install it without any autoloader by following steps below:

  • Download this repo or just download raw src/Block.php
  • Place it somewhere in your project. For example in yourproject/lib/Block.php.
  • Then include/require it to your code

Getting Started

Initialize Block

<?php

use Emsifa\Block;

require('vendor/autoload.php');

$view_dir = 'path/to/views';
$view_extension = 'block.php';

$block = new Block($view_dir, $view_extension);

By default $view_extension is php. We prefer to use custom extension. Custom extension make you easier to identify view files in your editor without open that file.

In this examples we use block.php, so our view filenames must be suffixed by .block.php instead just .php.

Your first template

Create file path/to/views/hello.block.php.

<h1><?= $title ?></h1>
<p>
  <?= $message ?>
</p>

Render it

Then somewhere in your code, you can render it with render method like this:

echo $block->render('hello', [
    'title' => 'Hello World'
    'message' => 'Lorem ipsum dolor sit amet'
]);

Yes. you don't need to put file extension in Block.

Result

Now the result should looks like this:

<h1>Hello World</h1>
<p>
  Lorem ipsum dolor sit amet
</p>

Extending and Blocking

Practically, there is two main view types in most template engines or template systems. Master view and page view.

Master view is a view that contain base html tags like <doctype>, <html>, <head>, <body>, etc. Page view is a view that extend master view and contain some blocks that defined in master view.

Note: Master view is not for rendered by render method. Master view is just for extended by any page views.

If you familiar with laravel blade syntax, here are the differences.

Blade Block
@extends('view-name') <?php $this->extend('view-name') ?>
@section('content') <?php $this->section('content') ?>
@stop <?php $this->stop() ?>
@show <?php $this->show() ?>
@parent <?php $this->parent() ?>
@yield('content') <?php echo $this->get('content') ?>

Here is simple real world case about extending and blocking

Create Master View

<!-- Stored in path/to/views/master.block.php -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><?= $title ?></title>
  <?= $this->section('stylesheets') ?>
  <link rel="stylesheet" href="bootstrap.css">
  <?= $this->show() ?>
</head>
<body>
  <header>
    <h1>App Name</h1>
  </header>
  <div id="content">
    <?= $this->get('content') ?>
  </div>
  <footer>
    &copy; 2016 - my app
  </footer>
  <?= $this->section('scripts') ?>
  <script src="jquery.js"></script>
  <script src="bootstrap.js"></script>
  <?= $this->show() ?>
</body>
</html>

In example above we use <?= instead <?php for $this->section and $this->show. It is ok because those methods doesn't return a value.

Create Page View

In master view above, there are block stylesheets, content, and scripts. So you need to define them in your page view.

<!-- Stored in path/to/views/pages/lorem-ipsum.block.php -->
<?= $this->extend('master') ?>

<?= $this->section('stylesheets') ?>
  <?= $this->parent() ?>
  <!-- senpai!! \(^o^) -->
  <link rel="stylesheet" href="lorem.css">
<?= $this->stop() ?>

<?= $this->section('scripts') ?>
  <?= $this->parent() ?>
  <!-- notice me too senpai!! (^o^)/ -->
  <script src="lorem.js"></script>
  <script>
    initPage();
  </script>
<?= $this->stop() ?>

<?= $this->section('content') ?>
<!-- notice me senpai!! \(^o^)/ -->
<p>
  Lorem ipsum dolor sit amet, consectetur adipisicing elit.
  Officiis, mollitia ad commodi.
  Eligendi saepe unde iusto quis, praesentium deleniti eos incidunt quas vero,
  voluptatem, reiciendis inventore, aliquam expedita et rerum.
</p>
<?= $this->stop() ?>

All blocks above are actually optional

Render It!

echo $block->render('pages.lorem-ipsum', [
  'title' => 'Lorem Ipsum'
]);

And the result should looks like this

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Lorem Ipsum</title>
  <link rel="stylesheet" href="bootstrap.css">
  <!-- senpai!! \(^o^) -->
  <link rel="stylesheet" href="lorem.css">
</head>
<body>
  <header>
    <h1>App Name</h1>
  </header>
  <div id="content">
    <!-- notice me senpai!! \(^o^) -->
    <p>
      Lorem ipsum dolor sit amet, consectetur adipisicing elit.
      Officiis, mollitia ad commodi.
      Eligendi saepe unde iusto quis, praesentium deleniti eos incidunt quas vero,
      voluptatem, reiciendis inventore, aliquam expedita et rerum.
    </p>
  </div>
  <footer>
    &copy; 2016 - my app
  </footer>
  <script src="jquery.js"></script>
  <script src="bootstrap.js"></script>
  <!-- notice me too senpai!! (^o^)/ -->
  <script src="lorem.js"></script>
  <script>
    initPage();
  </script>
</body>
</html>

Another Useful Stuffs

HTML Escaping

Like another template engine, Block also have shortcut for escaping HTML. In Block you can escaping HTML using $this->escape($html) or $e($html).

Example:

Render

$block->render('pages/sample-escaping', [
  'title' => 'Title <script>XSS.attack()</script>'
]);

View

<!-- Stored in path/to/views/pages/sample-escaping.block.php -->
<div>
  <h4><?= $e($title) ?></h4>
</div>

Then, title will be escaped like this:

<div>
  <h4>Title &lt;script&gt;XSS.attack()&lt;/script&gt;</h4>
</div>

$get($key, $default = NULL)

When rendering a view, we add variable $get that contain anonymous function. This function allows you to get a value passed by render method. If the key exists, it will return that value, and if not it will return default value (NULL).

For example in master view above, if you didn't set title in array, it will show an error undefined variable title. So to fix that, instead using isset like this

<title><?= isset($title) ? $title : 'Default Title' ?></title>

You can use $get like this:

<title><?= $get('title', 'Default Title') ?></title>

Note: $get also support dot notation. It mean, you can access array using dot as separator in $key.

For example you render a view with array data like this:

$block->render('pages/profile', [
  'user' => [
    'name' => 'John Doe'
  ]
])

You can use $get like this:

<div class='profile'>
  Name: <?= $get('user.name') ?>
  City: <?= $get('user.city.name', 'Unknown') ?>
</div>

In example above user.city.name will return 'Unknown' because you didn't set city in array user.

Include Partial View

There is another view type called partial view. Partial view is a view file containing partial layout that you can use in some page or master view like widget, sidebar, navbar, main-menu, etc. Partial view is like master view, it is not for rendered by render method. But you can render it by put it inside master or page view via put method.

In Blade, you can use @include('partial', $data) to include partial view. In block, you can use <?= $this->put('partial', $data) ?> instead.

For example, let's create a new page view that contain a widget slider.

First you need to create partial view for widget slider:

<!-- Stored in path/to/views/partials/slider.block.php -->

<!-- notice me senpai!! \(^o^)/ -->
<div class="widget widget-slider">
  <div class="slider-wrapper">
    <div class="slide-1">Slide 1</div>
    <div class="slide-2">Slide 2</div>
    <div class="slide-3">Slide 3</div>
  </div>
</div>

<?= $this->section('stylesheets') ?>
  <?= $this->parent() ?>
  <!-- senpai!! \(^o^) -->
  <link rel="stylesheet" href="slider.css">
<?= $this->stop() ?>

<?= $this->section('scripts') ?>
  <?= $this->parent() ?>
  <!-- senpai!! (^o^)/ -->
  <script src="slider.js"></script>
<?= $this->stop() ?>

Then you can include it in home page view using put method.

<!-- Stored in path/to/views/pages/home.block.php -->
<?= $this->extend('master') ?>

<?= $this->section('stylesheets') ?>
  <?= $this->parent() ?>
  <link rel="stylesheet" href="home.css">
<?= $this->stop() ?>

<?= $this->section('scripts') ?>
  <?= $this->parent() ?>
  <script src="home.js"></script>
  <script>
    initHomepage()
  </script>
<?= $this->stop() ?>

<?= $this->section('content') ?>
<div class="container">
  <?= $this->put('partials.slider') ?>
  <p>
    Lorem ipsum dolor sit amet, consectetur adipisicing elit.
    Officiis, mollitia ad commodi.
    Eligendi saepe unde iusto quis, praesentium deleniti eos incidunt quas vero,
    voluptatem, reiciendis inventore, aliquam expedita et rerum.
  </p>
</div>
<?= $this->stop() ?>

Now if you echo $block->render('pages.home'), the output should looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Default Title</title>
  <link rel="stylesheet" href="bootstrap.css">
  <!-- senpai!! \(^o^) -->
  <link rel="stylesheet" href="slider.css">
  <link rel="stylesheet" href="home.css">
</head>
<body>
  <header>
    <h1>App Name</h1>
  </header>
  <div id="content">
    <div class="container">
      <!-- notice me senpai!! \(^o^)/ -->
      <div class="widget widget-slider">
        <div class="slider-wrapper">
          <div class="slide-1">Slide 1</div>
          <div class="slide-2">Slide 2</div>
          <div class="slide-3">Slide 3</div>
        </div>
      </div>
      <p>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit.
        Officiis, mollitia ad commodi.
        Eligendi saepe unde iusto quis, praesentium deleniti eos incidunt quas vero,
        voluptatem, reiciendis inventore, aliquam expedita et rerum.
      </p>
    </div>
  </div>
  <footer>
    &copy; 2016 - my app
  </footer>
  <script src="jquery.js"></script>
  <script src="bootstrap.js"></script>
  <!-- senpai!! (^o^)/ -->
  <script src="slider.js"></script>
  <script src="home.js"></script>
  <script>
    initHomepage();
  </script>
</body>
</html>

Notice: slider.css and slider.js are placed in that order.

Add Directory Namespace

You can put second argument in setDirectory method to set namespaced directory.

For example, you have module admin that have its own views directory.

$block->setDirectory('path/to/admin/views', 'admin');

// then you can render it like this
$block->render('admin::pages.dashboard');

// and extend or put something in your view files like this
$this->extend('admin::master');
$this->put('admin::partials.some-chart');

View Composer

We have told you that Block is inspired by Blade right. So Block also have view composer like blade.

Sometimes you may have a view partial that have its own data. For example, think about navbar. In navbar, you want to display logged user name. So basically you need to pass data user name in all views who rendering that navbar. Alternatively, you may set user data inside navbar view. But set data inside view file is a bad practice.

So, the solution is using view composer. With composer, you can add some data to view before rendering that view.

Here is an example for that case:

First you need to register view composer for navbar using composer method.

$block->composer('partials.navbar', function($data) {
    // $data is data you passed into `render` or `put` method
    return [
        'username' => Auth::user()->username
    ];
});

Then in your navbar, you can do this

<!-- Stored in path/to/views/partials/navbar.block.php -->
<nav>
  <li>Some menu</li>
  ...
  <li>
    <?= $username ?>
  </li>
</nav>

So now, whenever navbar is rendered, composer will set variable username to it.

You can set first argument as array if you wanna set a composer to multiple views.

Component and Slot

This is new feature in Laravel 5.4 which inspired by Vue.js. Sometimes you may have partial view containing dynamic HTML. With put method, you can add HTML string in view data (second argument put). But, putting HTML code inside string is a bad practice, most text editors cannot highlight it.

So, this features allows you to write HTML that will be transformed to variable in partial view.

Think about alert, you may have alert which contain dynamic HTML inside it like this:

<!-- Stored in path/to/views/partials/alert.block.php -->
<div class="alert alert-<?= $type ?>">
  <h4><?= $title ?></h4>
  <?= $slot ?>
</div>

With put method, you need to pass slot and title variables like this:

<?php

$this->put('partials.alert', [
    'title' => 'Validation Errors <strong class="close">&times;</strong>',
    'type' => 'danger',
    'slot' => '
      <ul>
        <li>Email is required</li>
        <li>Password is required</li>
      </ul>
    '
]);

?>

It is ugly to put HTML inside string like that. With component and slot, you can put alert view like this:

<?= $this->component('partials.alert', ['type' => 'danger']) ?>
  <?= $this->slot('title') ?>
    Validation Errors <strong class="close">&times;</strong>
  <?= $this->endslot() ?>
  <ul>
    <li>Email is required</li>
    <li>Password is required</li>
  </ul>
<?= $this->endcomponent() ?>

Now code inside component will transformed into slot variable, and code inside slot('title') will transformed into title variable.

Extending with Data

For example you have case that some page using sidebar, and some page are not using it. You can pass data to extend method, so you don't need to pass it inside controller.

For example:

Master view

<!-- Stored in path/to/views/master.block.php -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><?= $title ?></title>
  <?= $this->section('stylesheets') ?>
  <link rel="stylesheet" href="bootstrap.css">
  <?= $this->show() ?>
</head>
<body>
  <header>
    <h1>App Name</h1>
  </header>
  <div id="content">
    <?= false === $get('sidebar') ? $this->put('sidebar') : '' ?>
    <?= $this->get('content') ?>
  </div>
  <footer>
    &copy; 2016 - my app
  </footer>
  <?= $this->section('scripts') ?>
  <script src="jquery.js"></script>
  <script src="bootstrap.js"></script>
  <?= $this->show() ?>
</body>
</html>

Page view

<?= $this->extend('master', ['sidebar' => false]) ?>

<?= $this->section('content') ?>
    <p>
        Your page content goes here
    </p>
<?= $this->stop() ?>

Append Section

Instead of do this:

<?= $this->section('scripts') ?>
    <?= $this->parent() ?>
    <script>alert('my scripts')</script>
<?= $this->stop() ?>

You can do this:

<?= $this->append('scripts') ?>
    <script>alert('my scripts')</script>
<?= $this->stop() ?>

Prepend Section

Instead of do this:

<?= $this->section('scripts') ?>
    <script>alert('my scripts')</script>
    <?= $this->parent() ?>
<?= $this->stop() ?>

You can do this:

<?= $this->prepend('scripts') ?>
    <script>alert('my scripts')</script>
<?= $this->stop() ?>

Write Code Once

For example you want to make partial view for datepicker, and want put datepicker script just once even you put() it multiple times, you can use method once().

Example

<!-- Stored in path/to/views/partials/datepicker.block.php -->
<div class="input-group">
  <div class="input-group-prepend">
    <span class="input-group-text"><i class="fa fa-calendar"></i></span>
  </div>
  <input type="text" class="form-control datepicker" name="<?= $name ?>"/>
</div>

<?= $this->append('styles') ?>
  <?= $this->once('datepicker-style') ?>
  <link rel="stylesheet" href="path/to/datepicker.min.css">
  <?= $this->endonce() ?>
<?= $this->stop() ?>

<?= $this->append('scripts') ?>
  <?= $this->once('datepicker-script') ?>
  <script src="path/to/datepicker.min.js"></script>
  <script>
    $('.datepicker').datepicker();
  </script>
  <?= $this->endonce() ?>
<?= $this->stop() ?>

With this approach, no matter how much you put('partials.datepicker'), datepicker script and css only rendered once!

Dot or Slash?

I love blade for template engine, but I can't always use blade in my project, especially in small projects. So I create this library to make it as similar as blade.

In blade you can use / or . to load view files. So block too. But we prefer you to use . instead / to make you easier remembering that you don't need to put view extension in block.