Explorar o código

Initial release of PurePlate v1.0.0

michelphp hai 1 día
achega
2c50bb274c

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/vendor/
+/.idea/
+.phpunit.result.cache

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 F. Michel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 83 - 0
README.md

@@ -0,0 +1,83 @@
+# PurePlate
+
+PurePlate is a lightweight and versatile template rendering library for native PHP.
+
+## Installation
+
+You can install the library using Composer. Just run the following command:
+
+```bash
+composer require michel/pure-plate
+```
+
+## Basic Usage
+
+To use the renderer in your project, first create an instance of the `PhpRenderer` class and pass the directory where your templates are located.
+
+```php
+use Michel\Renderer\PurePlate;
+
+// Specify the template directory
+$templateDir = '/path/to/templates';
+
+// Optional global variables to be passed to all templates
+$globals = [
+    'siteTitle' => 'My Website',
+];
+
+// Create the renderer instance
+$renderer = new PurePlate($templateDir, $globals);
+```
+
+### Creating a Layout
+
+Create a layout file (e.g., `layout.php`) that represents the common structure of your pages. Use `block()` to define sections that will be replaced by content from child templates.
+
+```php
+<!DOCTYPE html>
+<html>
+<head>
+    <title><?php echo $this->block('title'); ?></title>
+</head>
+<body>
+    <div class="container">
+        <?php echo $this->block('content'); ?>
+    </div>
+</body>
+</html>
+```
+
+### Creating a Template
+
+Create your template file (e.g., `page.php`). Use `extend()` to specify the layout file and `startBlock()` / `endBlock()` to define the content for the blocks.
+
+```php
+<?php $this->extend('layout.php'); ?>
+
+<?php $this->startBlock('title'); ?>
+    My Page Title
+<?php $this->endBlock(); ?>
+
+<?php $this->startBlock('content'); ?>
+    <h1>Hello, <?php echo $name; ?>!</h1>
+    <p>Welcome to my website.</p>
+<?php $this->endBlock(); ?>
+```
+
+### Rendering Templates
+
+To render your template, use the `render` method. You can pass an array of variables to be extracted and made available within the template.
+
+```php
+echo $renderer->render('page.php', ['name' => 'John']);
+```
+
+This will render `page.php`, inject its blocks into `layout.php`, and return the final HTML.
+
+## Contributing
+
+Contributions to the PurePlate library are welcome! If you find any issues or want to suggest enhancements, feel free to open a GitHub issue or submit a pull request.
+
+## License
+
+PurePlate is open-source software released under the MIT License. See the [LICENSE](LICENSE) file for more details.

+ 23 - 0
composer.json

@@ -0,0 +1,23 @@
+{
+  "name": "michel/pure-plate",
+  "description": "PurePlate is a lightweight and versatile template rendering library for native PHP.",
+  "type": "library",
+  "require": {
+    "php": ">=7.4"
+  },
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "F. Michel"
+    }
+  ],
+  "autoload": {
+    "psr-4": {
+      "Michel\\Renderer\\": "src",
+      "Test\\Michel\\Renderer\\": "tests"
+    }
+  },
+  "require-dev": {
+    "michel/unitester": "^1.0.0"
+  }
+}

+ 117 - 0
src/PurePlate.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Michel\Renderer;
+
+use InvalidArgumentException;
+use RuntimeException;
+use Throwable;
+use function extract;
+use function file_exists;
+use function ob_end_clean;
+use function ob_get_clean;
+use function ob_get_level;
+use function ob_start;
+use function rtrim;
+use function trim;
+
+final class PurePlate
+{
+    private string $templateDir;
+    private array $globals;
+
+    private array $blocks = [];
+    private ?string $currentBlock = null;
+    private ?string $layout = null;
+
+    public function __construct(
+        string $templateDir,
+        array  $globals = []
+    )
+    {
+        $this->templateDir = rtrim($templateDir, '/');
+        $this->globals = $globals;
+    }
+
+    public function render(string $view, array $context = []): string
+    {
+        $filename = $this->findTemplateFile($view);
+        $this->layout = null;
+
+        $level = ob_get_level();
+        ob_start();
+
+        try {
+            $context = $this->mergeContext($context);
+            extract($context);
+            include $filename;
+            $content = ob_get_clean();
+        } catch (Throwable $e) {
+            while (ob_get_level() > $level) {
+                ob_end_clean();
+            }
+            throw $e;
+        }
+
+        if ($this->layout === null) {
+            return $content;
+        }
+
+        return $this->render($this->layout);
+    }
+
+    public function extend(string $layout): void
+    {
+        $this->layout = $layout;
+    }
+
+    public function startBlock(string $name): void
+    {
+        if ($this->currentBlock !== null) {
+            throw new RuntimeException("A block is already started. Call endBlock() before starting a new block.");
+        }
+
+        $content = $this->block($name);
+        $this->currentBlock = $name;
+        if (!empty($content)) {
+            echo $content;
+        }
+        ob_start();
+    }
+
+    public function endBlock(): void
+    {
+        if ($this->currentBlock === null) {
+            throw new RuntimeException("No block started. Call startBlock() before calling endBlock().");
+        }
+        $this->blocks[$this->currentBlock] = trim(ob_get_clean());
+        $this->currentBlock = null;
+    }
+
+    public function block(string $name): string
+    {
+        return $this->blocks[$name] ?? '';
+    }
+
+    private function findTemplateFile(string $view): string
+    {
+        $filename = $this->templateDir . DIRECTORY_SEPARATOR . $view;
+        if (!file_exists($filename)) {
+            throw new InvalidArgumentException($filename . ' template not found');
+        }
+        return $filename;
+    }
+
+    private function mergeContext(array $context): array
+    {
+        foreach ($context as $key => $_) {
+            if (array_key_exists($key, $this->globals)) {
+                throw new InvalidArgumentException(sprintf(
+                    'Context key "%s" conflicts with a global variable',
+                    $key
+                ));
+            }
+        }
+
+        return array_merge($this->globals, $context);
+    }
+}

+ 77 - 0
tests/PurePlateTest.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace Test\Michel\Renderer;
+
+use Michel\Renderer\PurePlate;
+use Michel\UniTester\TestCase;
+
+final class PurePlateTest extends TestCase
+{
+    private PurePlate $renderer;
+
+    protected function setUp(): void
+    {
+        $this->renderer = new PurePlate(__DIR__ . '/views');
+    }
+
+    protected function tearDown(): void
+    {
+    }
+
+    protected function execute(): void
+    {
+       $this->testRenderBasicView();
+       $this->testRenderViewWithLayout();
+       $this->testRenderViewWithBlock();
+       $this->testRenderViewWithExtendingBlock();
+       $this->testStartAndEndBlocks();
+       $this->testRenderViewNotFound();
+    }
+
+    public function testRenderBasicView(): void
+    {
+        $expectedOutput = 'Hello, World!';
+        $output = $this->renderer->render('test.php', ['message' => 'Hello, World!']);
+        $this->assertEquals($expectedOutput, $output);
+    }
+
+    public function testRenderViewWithLayout(): void
+    {
+        $expectedOutput = '<!DOCTYPE html><html lang="fr"><head><title></title></head><body class="body"></body></html>';
+        $output = $this->renderer->render('test_layout.php');
+        $this->assertEquals($expectedOutput, $output);
+    }
+
+    public function testRenderViewWithBlock(): void
+    {
+        $expectedOutput = '';
+        $output = $this->renderer->render('test_block.php');
+        $this->assertEquals($expectedOutput, $output);
+    }
+
+    public function testRenderViewWithExtendingBlock(): void
+    {
+        $expectedOutput = '<!DOCTYPE html><html lang="fr"><head><title>Page Title</title></head><body class="body">Page Content</body></html>';
+        $output = $this->renderer->render('test_extends.php');
+        $this->assertEquals($expectedOutput, $output);
+    }
+
+    public function testStartAndEndBlocks(): void
+    {
+        $this->renderer->startBlock('content');
+        echo 'Block Content';
+        $this->renderer->endBlock();
+
+        $expectedOutput = 'Block Content';
+        $output = $this->renderer->block('content');
+        $this->assertEquals($expectedOutput, $output);
+    }
+
+    public function testRenderViewNotFound(): void
+    {
+        $this->expectException(\InvalidArgumentException::class, function () {
+            $this->renderer->render('non_existent_template.php');
+        });
+    }
+
+}

+ 2 - 0
tests/views/base.html.twig

@@ -0,0 +1,2 @@
+{% block body %}
+{% endblock %}

+ 2 - 0
tests/views/base.html.twig.php

@@ -0,0 +1,2 @@
+<?php $this->startBlock('body'); ?>
+<?php $this->endBlock(); ?>

+ 11 - 0
tests/views/instruction.html.twig

@@ -0,0 +1,11 @@
+{% if toto.user is not empty          and toto == 1 || php_sapi_name(strlen('toto'),(2 + 2) * trim('12')) === "cli not here" or toto is defined and true == (strlen('true') === 4) %}
+    affiche moi toto
+{% endif %}
+
+{% if users %}
+    <ul>
+        {% for user in users %}
+            <li>{{ user.username|e }}</li>
+        {% endfor %}
+    </ul>
+{% endif %}

+ 1 - 0
tests/views/test.php

@@ -0,0 +1 @@
+<?= $message ?>

+ 3 - 0
tests/views/test_block.php

@@ -0,0 +1,3 @@
+<?php $this->startBlock('content') ?>
+    Block Content
+<?php $this->endBlock() ?>

+ 9 - 0
tests/views/test_extends.php

@@ -0,0 +1,9 @@
+<?php $this->extend('test_layout.php') ?>
+
+<?php $this->startBlock('title') ?>
+Page Title
+<?php $this->endBlock() ?>
+
+<?php $this->startBlock('content') ?>
+Page Content
+<?php $this->endBlock() ?>

+ 1 - 0
tests/views/test_layout.php

@@ -0,0 +1 @@
+<!DOCTYPE html><html lang="fr"><head><title><?php echo $this->block('title'); ?></title></head><body class="body"><?php echo $this->block('content'); ?></body></html>

+ 61 - 0
tests/views/twig.html.twig

@@ -0,0 +1,61 @@
+{% extends 'base.html.twig' %}
+
+
+{% set user = "Alice" %}
+{% set discount = 15 %}
+{% set initialPrice = 200 %}
+{% set isMember = true %}
+{% set items = 5 %}
+{% set bonusPoints = 50 ? isMember : "tata" %}
+{% set toto = ['toto'] %}
+{% set toto2 = new stdClass() %}
+{{ isMember ? "Eligible for members special gift ?"|upper|lower : ":Regular item" }}
+
+{% block body %}
+
+Hello, {{ user }}!
+
+{% if isMember %}
+        {% set finalPrice = initialPrice - (initialPrice * discount / 100) %}
+        You have a discount of {{ discount }}%, so the final price is {{ finalPrice }} EUR.
+
+        {% if finalPrice < 100 %}
+                You're eligible for free shipping!
+        {% else %}
+                Shipping costs will apply.
+        {% endif %}
+{% else %}
+        No discount applies since you are not a member.
+        The price remains {{ initialPrice }} EUR.
+{% endif %}
+
+{% if items > 3 %}
+        As you have more than 3 items, you receive an additional 10 bonus points!
+        {% set bonusPoints = bonusPoints + 10 %}
+{% endif %}
+
+Your total bonus points: {{bonusPoints }}.
+
+{% for i in 1..items %}
+        - Item {{ i }}: {{ isMember ? "Eligible for members special gift ?"|upper : ":Regular item" }}\n
+        {% if i == 10 %}
+                This is an even-numbered item.
+        {% else %}
+                This is an odd-numbered item.
+        {% endif %}
+{% endfor %}
+
+{% if items == 0 %}
+        You have no items in your cart.
+{% elseif items == 1 %}
+        You have 1 item in your cart.
+{% else %}
+        You have {{ items + 41 }} items in your cart.
+{% endif %}
+
+{{ dump(isMember) }}
+{% if isMember === true %}
+{{ 'okkkkkkkkkkkkkkkk'|upper|lower }}
+{% endif %}
+
+{% endblock %}

+ 61 - 0
tests/views/twig.html.twig.php

@@ -0,0 +1,61 @@
+<?php $this->extend('base.html.twig.php'); ?>
+
+
+<?php $user = "Alice"; ?>
+<?php $discount = 15; ?>
+<?php $initialPrice = 200; ?>
+<?php $isMember = true; ?>
+<?php $items = 5; ?>
+<?php $bonusPoints = 50 ? $isMember : "tata"; ?>
+<?php $toto = ['toto']; ?>
+<?php $toto2 = new stdClass(); ?>
+<?php echo $isMember ? $__filters['lower']($__filters['upper']("Eligible for members special gift ?")) : ":Regular item"; ?>
+
+<?php $this->startBlock('body'); ?>
+
+Hello, <?php echo $user; ?>!
+
+<?php if ($isMember): ?>
+        <?php $finalPrice = $initialPrice - ( $initialPrice * $discount / 100 ); ?>
+        You have a discount of <?php echo $discount; ?>%, so the final price is <?php echo $finalPrice; ?> EUR.
+
+        <?php if ($finalPrice < 100): ?>
+                You're eligible for free shipping!
+        <?php else: ?>
+                Shipping costs will apply.
+        <?php endif; ?>
+<?php else: ?>
+        No discount applies since you are not a member.
+        The price remains <?php echo $initialPrice; ?> EUR.
+<?php endif; ?>
+
+<?php if ($items > 3): ?>
+        As you have more than 3 items, you receive an additional 10 bonus points!
+        <?php $bonusPoints = $bonusPoints + 10; ?>
+<?php endif; ?>
+
+Your total bonus points: <?php echo $bonusPoints; ?>.
+
+<?php foreach (range(1,$items) as $i): ?>
+        - Item <?php echo $i; ?>: <?php echo $isMember ? $__filters['upper']("Eligible for members special gift ?") : ":Regular item"; ?>\n
+        <?php if ($i == 10): ?>
+                This is an even-numbered item.
+        <?php else: ?>
+                This is an odd-numbered item.
+        <?php endif; ?>
+<?php endforeach; ?>
+
+<?php if ($items == 0): ?>
+        You have no items in your cart.
+<?php elseif ($items == 1): ?>
+        You have 1 item in your cart.
+<?php else: ?>
+        You have <?php echo $items + 41; ?> items in your cart.
+<?php endif; ?>
+
+<?php echo $__functions['dump'] ( $isMember ); ?>
+<?php if ($isMember === true): ?>
+<?php echo $__filters['lower']($__filters['upper']('okkkkkkkkkkkkkkkk')); ?>
+<?php endif; ?>
+
+<?php $this->endBlock(); ?>