bnomei/kirby3-handlebars

View on GitHub
classes/LncFiles.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace Bnomei;

use Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use LightnCandy\LightnCandy;

final class LncFiles
{
    /**
     * @var array
     */
    private $files;

    /**
     * @var string
     */
    private $modified;
    /**
     * @var array
     */
    private $options;

    /**
     * LncFiles constructor.
     * @param array $options
     */
    public function __construct(array $options = [])
    {
        $defaults = [
            'debug' => option('debug'),
            'compile-flags' => option('bnomei.handlebars.compile-flags'),
            'extension-input' => option('bnomei.handlebars.extension-input'),
            'extension-output' => option('bnomei.handlebars.extension-output'),
            'files' => option('bnomei.handlebars.files'),
            'lnc' => option('bnomei.handlebars.lnc'),
            'dir-templates' => option('bnomei.handlebars.dir-templates'),
            'dir-partials' => option('bnomei.handlebars.dir-partials'),
        ];
        $this->options = array_merge($defaults, $options);

        $this->options['files'] = $this->options['files'] && !$this->options['debug'];
        $this->options['lnc'] = $this->options['lnc'] && !$this->options['debug'];

        foreach ($this->options as $key => $call) {
            if (!is_string($call) && is_callable($call) && !in_array($call, ['hbs', 'handlebars'])) {
                $this->options[$key] = $call();
            }
        }

        if ($this->option('debug')) {
            $this->flush();
        }

        $this->files = [];
    }

    /**
     * @param null $key
     * @return array|mixed
     */
    public function option($key = null)
    {
        if ($key) {
            return A::get($this->options, $key);
        }
        return $this->options;
    }

    public function options(): array
    {
        return $this->options;
    }

    /**
     * @return array
     */
    public function compileOptions()
    {
        return [
            'flags' => $this->option('compile-flags'),
            'partialresolver' => function ($context, $name) {
                return self::singleton()->hbsOfPartial($name);
            },
        ];
    }

    /**
     * @param string $name
     * @return string
     */
    public function hbsOfPartial(string $name): string
    {
        foreach ($this->files as $lncFile) {
            if ($lncFile->partial() && $lncFile->name() === $name) {
                return $lncFile->hbs();
            }
        }
        return '';
    }

    /**
     * @param string $dir
     * @param string $extension
     * @return array
     */
    public function filterDirByExtension(string $dir, string $extension)
    {
        $result = [];
        foreach (Dir::files($dir, null, true) as $file) {
            if (F::extension($file) === $extension) {
                $result[] = $file;
            }
        }
        return $result;
    }

    /**
     * @param array $files
     * @return string
     */
    public function modified(array $files = []): string
    {
        if ($this->modified) {
            return $this->modified;
        }
        if (count($files) === 0) {
            $files = array_merge(
                $this->filterDirByExtension(
                    (string)$this->option('dir-templates'),
                    (string)$this->option('extension-input')
                ),
                $this->filterDirByExtension(
                    (string)$this->option('dir-partials'),
                    (string)$this->option('extension-input')
                )
            );
        }

        $modified = ['LncFilesSalt'];
        foreach ($files as $file) {
            $modified[] = F::modified($file);
        }

        $this->modified = strval(crc32(implode($modified)));
        return $this->modified;
    }

    /**
     * @return array
     */
    public function scan(): array
    {
        $files = [];
        $dirs = [
            [$this->option('dir-templates'), false],
            [$this->option('dir-partials'), true],
        ];

        foreach ($dirs as $dir) {
            $templates = $this->filterDirByExtension(
                (string)$dir[0],
                (string)$this->option('extension-input')
            );
            // first get all
            foreach ($templates as $file) {
                $name = basename($file, '.' . $this->option('extension-input'));
                // ignore all files starting with _ (like fractals.build _preview.hbs)
                if (substr($name, 0, 1) === '_') {
                    continue;
                }
                $files[] = new LncFile([
                    'name' => $name,
                    'source' => $file,
                    'target' => $this->target($file, $dir[1]),
                    'partial' => $dir[1],
                    'modified' => F::modified($file),
                    'lnc' => $this->option('lnc'),
                ]);
            }
        }

        return $files;
    }

    /**
     * @param LncFile $lncFile
     * @return false|string
     */
    public function compile(LncFile $lncFile)
    {
        return LightnCandy::compile(
            $lncFile->hbs(),
            $this->compileOptions()
        );
    }

    /**
     * @param string $file
     * @param bool $partial
     * @return string
     */
    public function target(string $file, bool $partial = false)
    {
        $path = [
            $this->lncCacheRoot(),
            DIRECTORY_SEPARATOR,
            ($partial ? '@' : ''),
            basename($file, '.' . $this->option('extension-input')),
            '.' . $this->option('extension-output'),
        ];
        return implode($path);
    }

    /**
     * @return array
     */
    public function load()
    {
        $files = [];

        if ($this->option('files')) {
            $files = kirby()->cache('bnomei.handlebars.files')->get(
                $this->modified(),
                []
            );
            $files = array_map(function ($file) {
                return new LncFile($file);
            }, $files);
        }
        if (count($files)) {
            return $files;
        }

        return $this->scan();
    }

    /**
     * @param array $files
     * @return bool
     */
    public function write(array $files): bool
    {
        if (!$this->option('files')) {
            return false;
        }
        return kirby()->cache('bnomei.handlebars.files')->set(
            $this->modified(),
            array_map(function ($file) {
                return $file->toArray();
            }, $files)
        );
    }

    /**
     * @return array
     */
    public function registerAllTemplates(): array
    {
        $this->files = $this->load();

        $anyPartialNeedsUpdate = false;
        foreach ($this->files as $lncFile) {
            if ($lncFile->partial() && $lncFile->needsUpdate()) {
                $anyPartialNeedsUpdate = true;
                $lncFile->writePartial();
            }
        }

        foreach ($this->files as $lncFile) {
            if (!$lncFile->partial() && ($anyPartialNeedsUpdate || $lncFile->needsUpdate())) {
                $php = $this->compile($lncFile);
                if (is_string($php)) {
                    $lncFile->php($php);
                }
            }
        }

        $this->write($this->files);
        return $this->files;
    }

    /**
     * @param string $name
     * @return string
     * @throws InvalidArgumentException
     */
    public function lncFile(string $name): string
    {
        foreach ($this->files as $lncFile) {
            if ($lncFile->name() === $name) {
                return $lncFile->target();
            }
        }
        if ($name === 'default') {
            throw new InvalidArgumentException(); // @codeCoverageIgnore
        }
        return $this->lncFile('default');
    }

    /**
     * @param string $name
     * @return string
     * @throws InvalidArgumentException
     */
    public function hbsFile(string $name): string
    {
        foreach ($this->files as $lncFile) {
            if ($lncFile->name() === $name) {
                return F::realpath($lncFile->source());
            }
        }
        if ($name === 'default') {
            throw new InvalidArgumentException(); // @codeCoverageIgnore
        }
        return $this->hbsFile('default');
    }

    /**
     * @param $name
     * @return string
     */
    public function precompiledTemplate($name): string
    {
        foreach ($this->files as $lncFile) {
            if ($lncFile->partial() === false && $lncFile->name() === $name) {
                $php = $lncFile->php();
                if (! $php) {
                    $php = $this->compile($lncFile);
                }
                return $php;
            }
        }
        return $this->precompiledTemplate('default');
    }

    /**
     * @return string
     */
    public function lncCacheRoot(): string
    {
        return kirby()->cache('bnomei.handlebars')->root();
    }

    /**
     *
     */
    public function flush()
    {
        try {
            kirby()->cache('bnomei.handlebars.files')->flush();

            foreach (Dir::read($this->lncCacheRoot()) as $file) {
                $file = $this->lncCacheRoot() . '/' . $file;
                if (is_file($file)) {
                    @unlink($file);
                }
            }
        } catch (Exception $e) {
            //
        }
    }

    /**
     * @return mixed
     */
    public function files()
    {
        return $this->files;
    }

    /*
     * @var LncFiles
     */
    private static $singleton;

    /**
     * @param array $options
     * @return LncFiles
     * @codeCoverageIgnore
     */
    public static function singleton(array $options = [])
    {
        if (!self::$singleton) {
            self::$singleton = new self($options);
            self::$singleton->registerAllTemplates();
        }

        return self::$singleton;
    }
}