Build your own PHP framework -- Build a template engine (3)

Keywords: PHP github Laravel

Previously, we implemented the simplest template replacement for echo commands, which is to replace {$name} with <? PHP echo $name?>.

Now let's go through the other commands and review the previous definitions.

  • Output variable value

The return value of the {{}} expression will be automatically passed to the htmlentities function of PHP for processing to prevent XSS attacks.

Hello, {{ $name }}!
  • Output Unescaped Variable Values
Hello, {!! $name !!}!
  • If expression

If expressions can be created through @if,@elseif,@else and @endif instructions.

@if (count($records) === 1)
    I have one record!
@elseif (count($records) > 1)
    I have multiple records!
@else
    I don't have any records!
@endif
  • loop
@for ($i = 0; $i < 10; $i++)
    The current value is {{ $i }}
@endfor

@foreach ($users as $user)
    <p>This is user {{ $user->id }}</p>
@endforeach

@while (true)
    <p>I'm looping forever.</p>
@endwhile
  • Introduce other views
@include('view.name', ['some' => 'data'])

To match these definitions, we need to write the corresponding regular expression, and the command at the beginning of @ takes the use of laravel directly.

We first create the view folder under src, and then create the Compiler class file.

We will compiler in two ways: Statements at the beginning of @ and Echos. The two are different.

Firstly, the variable compliers are defined as follows:

protected $compilers = [
    'Statements',
    'Echos',
];

Then migrate to the Complier class according to the render method in the original Controller, and match the two in turn. The code is as follows:

public function compile($path = null)
{
    $fileContent = file_get_contents($path);
    $result = '';
    foreach (token_get_all($fileContent) as $token) {
        if (is_array($token)) {
            list($id, $content) = $token;
            if ($id == T_INLINE_HTML) {
                foreach ($this->compilers as $type) {
                    $content = $this->{"compile{$type}"}($content);
                }
            }
            $result .= $content;
        } else {
            $result .= $token;
        }
    }
    $generatedFile = '../runtime/cache/' . md5($path);
    file_put_contents($generatedFile, $result);
    require_once $generatedFile;
}

protected function compileStatements($content)
{
    return $content;
}

protected function compileEchos($content)
{
    return preg_replace('/{{(.*)}}/', '<?php echo $1 ?>', $content);
}

Statements are completely untreated, and Echos are the same as before.

First adjust the processing in Echos, add the names of variable records {{} and {!!!}

protected $echoCompilers = [
    'RawEchos',
    'EscapedEchos'
];

When processing, you can add a judgment of existence. The default value is null. The content can be adjusted as follows:

protected function compileEchos($content)
{
    foreach ($this->echoCompilers as $type) {
        $content = $this->{"compile{$type}"}($content);
    }
    return $content;
}

protected function compileEscapedEchos($content)
{
    return preg_replace('/{{(.*)}}/', '<?php echo htmlentities(isset($1) ? $1 : null) ?>', $content);
}

protected function compileRawEchos($content)
{
    return preg_replace('/{!!(.*)!!}/', '<?php echo isset($1) ? $1 : null ?>', $content);
}

The difference between EscapedEchos and RawEchos is that the first one does html escape.

Let's look at the processing of the Statements command again. The principle is the same. It matches the corresponding commands, such as if and foreach, and calls the corresponding methods to replace them.

The code is as follows:

protected function compileStatements($content)
{
    return preg_replace_callback(
            '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', function ($match) {
            return $this->compileStatement($match);
        }, $content
    );
}

protected function compileStatement($match)
{
    if (strpos($match[1], '@') !== false) {
        $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1];
    } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) {
        $match[0] = $this->$method(isset($match[3]) ? $match[3] : null);
    }
    
    return isset($match[3]) ? $match[0] : $match[0].$match[2];
}

protected function compileIf($expression)
{
    return "<?php if{$expression}: ?>";
}

protected function compileElseif($expression)
{
    return "<?php elseif{$expression}: ?>";
}

protected function compileElse($expression)
{
    return "<?php else{$expression}: ?>";
}

protected function compileEndif($expression)
{
    return '<?php endif; ?>';
}

protected function compileFor($expression)
{
    return "<?php for{$expression}: ?>";
}

protected function compileEndfor($expression)
{
    return '<?php endfor; ?>';
}

protected function compileForeach($expression)
{
    return "<?php foreach{$expression}: ?>";
}

protected function compileEndforeach($expression)
{
    return '<?php endforeach; ?>';
}

protected function compileWhile($expression)
{
    return "<?php while{$expression}: ?>";
}

protected function compileEndwhile($expression)
{
    return '<?php endwhile; ?>';
}

protected function compileContinue($expression)
{
    return '<?php continue; ?>';
}

protected function compileBreak($expression)
{
    return '<?php break; ?>';
}

include among them is more difficult to implement, so we haven't done it. Let's think about it.

Then, let's reconsider that it's impossible to operate on file regeneration every time. I should judge that the file has changed, and if it hasn't changed, use the cache directly.

The adjustment code is as follows:

public function isExpired($path)
{
    $compiled = $this->getCompiledPath($path);
    if (!file_exists($compiled)) {
        return true;
    }
    
    return filemtime($path) >= filemtime($compiled);
}

protected function getCompiledPath($path)
{
    return '../runtime/cache/' . md5($path);
}

public function compile($file = null, $params = [])
{
    $path = '../views/' . $file . '.sf';
    extract($params);
    if (!$this->isExpired($path)) {
        $compiled = $this->getCompiledPath($path);
        require_once $compiled;
        return;
    }
    $fileContent = file_get_contents($path);
    $result = '';
    foreach (token_get_all($fileContent) as $token) {
        if (is_array($token)) {
            list($id, $content) = $token;
            if ($id == T_INLINE_HTML) {
                foreach ($this->compilers as $type) {
                    $content = $this->{"compile{$type}"}($content);
                }
            }
            $result .= $content;
        } else {
            $result .= $token;
        }
    }
    $compiled = $this->getCompiledPath($path);
    file_put_contents($compiled, $result);
    require_once $compiled;
}

This series of blogs has come to an end for the time being.

Project content and blog content will also be put on Github, welcome to make suggestions.

code: https://github.com/CraryPrimitiveMan/simple-framework/tree/1.2

blog project: https://github.com/CraryPrimitiveMan/create-your-own-php-framework

Posted by Randuin on Tue, 11 Jun 2019 15:27:37 -0700