Initial import.

This commit is contained in:
flash 2023-08-24 22:31:36 +00:00
commit 5e2c842434
24 changed files with 3383 additions and 0 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
* text=auto
*.sh text eol=lf
*.php text eol=lf
*.bat text eol=crlf

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
[Tt]humbs.db
[Dd]esktop.ini
.DS_Store
.vscode/
.vs/
.idea/
docs/html/
.phpdoc*
.phpunit*
vendor/

30
LICENCE Normal file
View file

@ -0,0 +1,30 @@
Copyright (c) 2023, flashwave <me@flash.moe>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# Sasae
Sasae is a simple wrapper around some of Twig's functionality as well as making `Twig\Environment` a little bit more immutable.
While it's not a lot of extras, I often implement the added functionality across projects.
The source file structure is meant to be similar to Twig's own.
## Requirements and Dependencies
Sasae currently targets **PHP 8.2**.
No additional requirements and/or dependencies at this time.
## Versioning
Sasae versioning will follows the [Semantic Versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html).
Changes to minimum required PHP version, major Twig library releases that cause incompatibilities and other major overhauls to Sasae itself that break compatibility will be reasons for incrementing the major version.
Updates to Sasae functionality or adjustments to fit new Twig functionality will cause increment the minor version.
Bug fixes and inconsequential Twig library updates will increment the patch version.
Sasae also depends on Index, but its versioning depends on the minimum PHP version and should thus be fairly inconsequential.
Previous major versions may be supported for a time with backports depending on what projects of mine still target older versions of PHP.
The version is stored in the root of the repository in a file called `VERSION` and can be read out within Sasae using `Sasae\SasaeEnvironment::getSasaeVersion()`.
## Contribution
By submitting code for inclusion in the main Sasae source tree you agree to transfer ownership of the code to the project owner.
The contributor will still be attributed for the contributed code, unless they ask for this attribution to be removed.
This is to avoid intellectual property rights traps and drama that could lead to blackmail situations.
If you do not agree with these terms, you are free to fork off.
## Licencing
Sasae is available under the BSD 3-Clause Clear License, a full version of which is enclosed in the LICENCE file.

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.0.0-dev

31
composer.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "flashwave/sasae",
"description": "A wrapper for Twig with added common functionality.",
"type": "library",
"license": "bsd-3-clause-clear",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"twig/twig": "^3.7",
"twig/html-extra": "^3.7",
"flashwave/index": "dev-master"
},
"require-dev": {
"phpunit/phpunit": "^10.2",
"phpstan/phpstan": "^1.10"
},
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"autoload": {
"psr-4": {
"Sasae\\": "src"
}
}
}

2437
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

41
phpdoc.xml Normal file
View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" ?>
<phpdocumentor configVersion="3.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.phpdoc.org"
xsi:noNamespaceSchemaLocation="data/xsd/phpdoc.xsd">
<title>Sasae Documentation</title>
<template name="default"/>
<paths>
<output>docs/html</output>
</paths>
<version number="0.1">
<folder>latest</folder>
<api format="php">
<output>api</output>
<visibility>public</visibility>
<default-package-name>Sasae</default-package-name>
<source dsn=".">
<path>src</path>
</source>
<extensions>
<extension>php</extension>
</extensions>
<ignore hidden="true" symlinks="true">
<path>tests/**/*</path>
</ignore>
<ignore-tags>
<ignore-tag>template</ignore-tag>
<ignore-tag>template-extends</ignore-tag>
<ignore-tag>template-implements</ignore-tag>
<ignore-tag>extends</ignore-tag>
<ignore-tag>implements</ignore-tag>
</ignore-tags>
</api>
<guide format="rst">
<source dsn=".">
<path>docs</path>
</source>
<output>guide</output>
</guide>
</version>
</phpdocumentor>

7
phpstan.neon Normal file
View file

@ -0,0 +1,7 @@
parameters:
level: 9
checkUninitializedProperties: true
checkImplicitMixed: true
checkBenevolentUnionTypes: true
paths:
- src

23
phpunit.xml Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
colors="true"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true"
cacheDirectory=".phpunit.cache"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true">
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View file

@ -0,0 +1,35 @@
<?php
namespace Sasae\Cache;
use Twig\Cache\FilesystemCache as TwigFilesystemCache;
/**
* Extends Twig's filesystem cache implementation with an alternate constructor.
*/
class SasaeFilesystemCache extends TwigFilesystemCache {
/**
* string $path Directory path to store the cache in.
* bool $autoReload Whether to refresh the cache if changes are detected.
*/
public function __construct(string $path, bool $autoReload) {
parent::__construct(
$path,
$autoReload ? TwigFilesystemCache::FORCE_BYTECODE_INVALIDATION : 0
);
}
/**
* Creates an instance of the filesystem cacher in the system temporary path based on project name and version.
*
* @param string $name Name of the project in a format the filesystem will be happy with.
* @param ?string $version Version of the project in a format the filesystem will be happy with or null to enable auto reload.
*/
public static function create(string $name, ?string $version): self {
$autoReload = $version === null;
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'sasae-' . $name;
if(!$autoReload)
$path .= '-' . $version;
return new self($path, $autoReload);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Sasae\Extension;
use Index\ByteFormat;
use Index\Environment as NdxEnvironment;
use Sasae\SasaeEnvironment;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\Extension\AbstractExtension as TwigAbstractExtension;
/**
* Provides version functions and additional functionality implemented in Index.
*/
class SasaeExtension extends TwigAbstractExtension {
public function getFilters() {
return [
new TwigFilter('format_filesize', ByteFormat::format(...)),
];
}
public function getFunctions() {
return [
new TwigFunction('ndx_version', NdxEnvironment::getIndexVersion(...)),
new TwigFunction('sasae_version', SasaeEnvironment::getSasaeVersion(...)),
new TwigFunction('twig_version', SasaeEnvironment::getTwigVersion(...)),
];
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Sasae\Loader;
use InvalidArgumentException;
use Twig\Source;
use Twig\Error\LoaderError;
use Twig\Loader\LoaderInterface;
/**
* Provides a simpler Filesystem loader with mechanisms like namespaces omitted.
*/
class SasaeFilesystemLoader implements LoaderInterface {
private string $root;
/**
* @param string $path Base path to the templates directory.
*/
public function __construct(string $path) {
$path = realpath($path);
if($path === false)
throw new InvalidArgumentException('$path does not exist.');
$this->root = $path;
}
/**
* Returns the underlying path.
*
* @return string
*/
public function getPath(): string {
return $this->root;
}
/** @var array<string, string> */
private array $absPaths = [];
private function getAbsolutePath(string $path, bool $throw): string {
$cachePath = $path;
if(array_key_exists($cachePath, $this->absPaths))
return $this->absPaths[$cachePath];
if(pathinfo($path, PATHINFO_EXTENSION) === '')
$path = rtrim($path, '.') . '.twig';
$absPath = realpath($this->root . DIRECTORY_SEPARATOR . $path);
if($absPath === false) {
if(!$throw)
return '';
throw new LoaderError(sprintf('Could not find template "%s" in "%s".', $path, $this->root));
}
if(!str_starts_with($absPath, $this->root)) {
if(!$throw)
return '';
throw new LoaderError(sprintf('Attempting to load "%s" which is outside of the template directory.', $absPath));
}
return $this->absPaths[$cachePath] = $absPath;
}
public function getSourceContext(string $name): Source {
$path = $this->getAbsolutePath($name, true);
$body = file_get_contents($path);
if($body === false)
throw new LoaderError(sprintf('Was unable to read "%s"', $path));
return new Source($body, $name, $path);
}
public function getCacheKey(string $name): string {
return $this->getAbsolutePath($name, true);
}
public function isFresh(string $name, int $time): bool {
return filemtime($this->getAbsolutePath($name, true)) < $time;
}
public function exists(string $name) {
return $this->getAbsolutePath($name, false) !== '';
}
}

90
src/SasaeContext.php Normal file
View file

@ -0,0 +1,90 @@
<?php
namespace Sasae;
use InvalidArgumentException;
use Stringable;
use Twig\TemplateWrapper as TwigTemplateWrapper;
/**
* Provides a wrapper of Twig\TemplateWrapper.
*/
class SasaeContext implements Stringable {
/**
* @internal
* @param array<string, mixed> $vars
*/
public function __construct(
private TwigTemplateWrapper $wrapper,
private array $vars = []
) {}
/**
* Returns the underlying wrapper instance.
*
* @return TwigTemplateWrapper
*/
public function getWrapper(): TwigTemplateWrapper {
return $this->wrapper;
}
/**
* Sets a local variable.
*
* $path is evaluated to allow accessing deeper layer arrays without overwriting it entirely.
*
* @param string $path Array path to the variable.
* @param mixed $value Desired value.
*/
public function setVar(string $path, mixed $value): void {
$path = explode('.', $path);
$target = &$this->vars;
$targetName = array_pop($path);
if(!empty($path)) {
$path = array_reverse($path);
while(($name = array_pop($path)) !== null) {
if(!is_array($target))
throw new InvalidArgumentException('The $path you\'re attempting to write to conflicts with a non-array type.');
if(!array_key_exists($name, $target))
$target[$name] = [];
$target = &$target[$name];
}
}
if(!is_array($target))
throw new InvalidArgumentException('The $path you\'re attempting to write to conflicts with a non-array type.');
$target[$targetName] = $value;
}
/**
* Merges a set of variables into the local variable set.
*
* @param array<string, mixed> $vars Variables to apply to the set.
*/
public function setVars(array $vars): void {
$this->vars = array_merge($this->vars, $vars);
}
/**
* Renders the template to a string, taking additional variables that are not commit to local set.
*
* @param ?array<string, mixed> $vars Additional local variables, nullable to avoid additional function calls.
* @return string Rendered template.
*/
public function render(?array $vars = null): string {
return $this->wrapper->render(
$vars === null ? $this->vars : array_merge($this->vars, $vars)
);
}
/**
* Renders the template to a string.
*
* @return string Rendered template.
*/
public function __toString(): string {
return $this->render();
}
}

192
src/SasaeEnvironment.php Normal file
View file

@ -0,0 +1,192 @@
<?php
// SasaeEnvironment.php
// Created: 2023-08-24
// Updated: 2023-08-24
namespace Sasae;
use UnexpectedValueException;
use Index\Version;
use Sasae\Extension\SasaeExtension;
use Twig\Environment as TwigEnvironment;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
use Twig\Cache\CacheInterface as TwigCacheInterface;
use Twig\Extension\ExtensionInterface as TwigExtensionInterface;
use Twig\Extra\Html\HtmlExtension as TwigHtmlExtension;
use Twig\Loader\LoaderInterface as TwigLoaderInterface;
/**
* Provides a wrapper of Twig\Environment.
*/
class SasaeEnvironment {
private TwigEnvironment $env;
private static ?string $sasaeVersionString = null;
private static ?Version $sasaeVersion = null;
private static ?Version $twigVersion = null;
/**
* @param TwigLoaderInterface $loader A template loader instance.
* @param ?TwigCacheInterface $cache A caching driver.
* @param string $charset Character for templates.
* @param bool $debug Debug mode.
*/
public function __construct(
TwigLoaderInterface $loader,
?TwigCacheInterface $cache = null,
string $charset = 'utf-8',
bool $debug = false
) {
$this->env = new TwigEnvironment($loader, [
'debug' => $debug,
'cache' => $cache,
'charset' => $charset,
'strict_variables' => true, // there's no reason to disable this ever
]);
$this->env->addExtension(new TwigHtmlExtension);
$this->env->addExtension(new SasaeExtension);
}
/**
* Get a reference to the underlying Twig Environment.
* Things that aren't exposed through Sasae generally have a reason
* for being "obfuscated" but go wild if you really want to.
*
* @return TwigEnvironment
*/
public function getEnvironment(): TwigEnvironment {
return $this->env;
}
/**
* Returns if debug mode is enabled.
*
* @return bool
*/
public function isDebug(): bool {
return $this->env->isDebug();
}
/**
* Registers an extension.
*
* @param TwigExtensionInterface $extension
*/
public function addExtension(TwigExtensionInterface $extension): void {
$this->env->addExtension($extension);
}
/**
* Registers a filter.
*
* @param string $name Name of the filter.
* @param callable $body Body of the filter.
* @param array<string, mixed> $options Options, review the TwigFilter file for the options.
*/
public function addFilter(string $name, callable $body, array $options = []): void {
$this->env->addFilter(new TwigFilter($name, $body, $options));
}
/**
* Registers a function.
*
* @param string $name Name of the function.
* @param callable $body Body of the function.
* @param array<string, mixed> $options Options, review the TwigFunction file for the options.
*/
public function addFunction(string $name, callable $body, array $options = []): void {
$this->env->addFunction(new TwigFunction($name, $body, $options));
}
/**
* Registers a twig.
*
* @param string $name Name of the twig.
* @param callable $body Body of the twig.
* @param array<string, mixed> $options Options, review the TwigTest file for the options.
*/
public function addTest(string $name, callable $body, array $options = []): void {
$this->env->addTest(new TwigTest($name, $body, $options));
}
/**
* Adds a global variable available in any SasaeContext instance.
*
* @param string $name Name of the variable.
* @param mixed $value Content of the variable.
*/
public function addGlobal(string $name, mixed $value): void {
$this->env->addGlobal($name, $value);
}
/**
* Loads a template and creates a SasaeContext instance.
*
* @param string $name Name or path of the template.
* @param array<string, mixed> $vars Context local variables to add right away.
* @return SasaeContext
*/
public function load(string $name, array $vars = []): SasaeContext {
return new SasaeContext($this->env->load($name), $vars);
}
/**
* Direct proxy to TwigEnvironment's render method.
*
* @param string $name Name or path of the template.
* @param array<string, mixed> $vars Local variables to render the template with.
* @return string
*/
public function render(string $name, array $vars = []): string {
return $this->env->render($name, $vars);
}
/**
* Returns the current version of the Sasae library.
*
* @return Version
*/
public static function getSasaeVersion(): Version {
if(self::$sasaeVersion === null)
self::$sasaeVersion = Version::parse(self::getSasaeVersionString());
return self::$sasaeVersion;
}
/**
* Returns the current version of the Sasae library as a string.
*
* @return string
*/
public static function getSasaeVersionString(): string {
if(self::$sasaeVersionString === null) {
$body = file_get_contents(__DIR__ . '/../VERSION');
if($body === false)
throw new UnexpectedValueException('Was unable to read VERSION file.');
self::$sasaeVersionString = trim($body);
}
return self::$sasaeVersionString;
}
/**
* Returns the current version of the Twig library.
*
* @return Version
*/
public static function getTwigVersion(): Version {
if(self::$twigVersion === null)
self::$twigVersion = Version::parse(TwigEnvironment::VERSION);
return self::$twigVersion;
}
/**
* Returns the current version of the Twig library as a string.
*
* @return string
*/
public static function getTwigVersionString(): string {
return TwigEnvironment::VERSION;
}
}

66
tests/SasaeTest.php Normal file
View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Index\Environment;
use Index\XString;
use Sasae\SasaeContext;
use Sasae\SasaeEnvironment;
use Sasae\Cache\SasaeFilesystemCache;
use Sasae\Extension\SasaeExtension;
use Sasae\Loader\SasaeFilesystemLoader;
/**
* @covers SasaeContext
* @covers SasaeEnvironment
* @covers SasaeExtension
* @covers SasaeFilesystemCache
* @covers SasaeFilesystemLoader
*/
final class SasaeTest extends TestCase {
public function testEverything(): void {
$env = new SasaeEnvironment(
new SasaeFilesystemLoader(__DIR__),
SasaeFilesystemCache::create('SasaeTest', XString::random(8))
);
$this->assertFalse($env->isDebug());
$env->addGlobal('global_var', 'Sasae global var');
$env->addGlobal('expect', [
'ndx_version' => (string)Environment::getIndexVersion(),
'sasae_version' => SasaeEnvironment::getSasaeVersionString(),
'twig_version' => SasaeEnvironment::getTwigVersionString(),
]);
$env->addFilter('test_filter', fn($text) => ('filter:' . $text));
$env->addFunction('test_function', fn($text) => ('func:' . $text));
$env->addTest('test_test', fn($text) => $text === 'test');
$rendered = $env->render('test-rendered', [
'local_var' => 'this var is local',
]);
$this->assertEquals(file_get_contents(__DIR__ . '/test-rendered.html'), $rendered);
$ctx = $env->load('test-loaded', [
'context_var' => 'this var is context',
'variant' => 'toString()',
]);
$ctx->setVar('simple_set', 'simple set call');
$ctx->setVar('another.context.var.deep', 'applied with fuckery');
$ctx->setVars([
'context_var2' => 'applied without fuckery',
]);
$loaded = $ctx->render([
'local_var' => 'this var is local',
'variant' => 'render()',
]);
$this->assertEquals(file_get_contents(__DIR__ . '/test-loaded-render.html'), $loaded);
$this->assertEquals(file_get_contents(__DIR__ . '/test-loaded-string.html'), (string)$ctx);
}
}

13
tests/test-global.twig Normal file
View file

@ -0,0 +1,13 @@
{% set ndx_version_output = ndx_version() %}
{% set sasae_version_output = sasae_version() %}
{% set twig_version_output = twig_version() %}
ndx_version {{ ndx_version_output == expect.ndx_version ? 'works!' : ('returned "' ~ ndx_version_output ~ '" instead of "' ~ expect.ndx_version ~ '".') }}
sasae_version {{ sasae_version_output == expect.sasae_version ? 'works!' : ('returned "' ~ sasae_version_output ~ '" instead of "' ~ expect.sasae_version ~ '".') }}
twig_version {{ twig_version_output == expect.twig_version ? 'works!' : ('returned "' ~ twig_version_output ~ '" instead of "' ~ expect.twig_version ~ '".') }}
global_var = {{ global_var }}
{{ 'meow'|test_filter }}
{{ test_function('the') }}
{{ 'test' is test_test ? 'test works' : 'test does not work' }}

View file

@ -0,0 +1,19 @@
Loaded Template render()
ndx_version works!
sasae_version works!
twig_version works!
global_var = Sasae global var
filter:meow
func:the
test works
this var is context
simple set call
applied with fuckery
applied without fuckery
this was called through render() so local_var is set to "this var is local"

View file

@ -0,0 +1,19 @@
Loaded Template toString()
ndx_version works!
sasae_version works!
twig_version works!
global_var = Sasae global var
filter:meow
func:the
test works
this var is context
simple set call
applied with fuckery
applied without fuckery
this was called through toString() so local_var isn't defined

14
tests/test-loaded.twig Normal file
View file

@ -0,0 +1,14 @@
Loaded Template {{ variant }}
{% include 'test-global' %}
{{ context_var }}
{{ simple_set }}
{{ another.context.var.deep }}
{{ context_var2 }}
{% if local_var is defined %}
this was called through render() so local_var is set to "{{ local_var }}"
{% else %}
this was called through toString() so local_var isn't defined
{% endif %}

14
tests/test-rendered.html Normal file
View file

@ -0,0 +1,14 @@
Rendered Template
ndx_version works!
sasae_version works!
twig_version works!
global_var = Sasae global var
filter:meow
func:the
test works
this var is local

5
tests/test-rendered.twig Normal file
View file

@ -0,0 +1,5 @@
Rendered Template
{% include 'test-global' %}
{{ local_var }}

8
tools/precommit.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
pushd .
cd $(dirname "$0")
php update-headers.php
popd

172
tools/update-headers.php Normal file
View file

@ -0,0 +1,172 @@
<?php
// the point of index was so that i wouldn't have to copy things between projects
// here i am copying things from index
date_default_timezone_set('utc');
function git_changes(): array {
$files = [];
$dir = getcwd();
try {
chdir(__DIR__ . '/..');
$output = explode("\n", trim(shell_exec('git status --short --porcelain=v1 --untracked-files=all --no-column')));
foreach($output as $line) {
$line = trim($line);
$file = realpath(explode(' ', $line)[1]);
$files[] = $file;
}
} finally {
chdir($dir);
}
return $files;
}
function collect_files(string $directory): array {
$files = [];
$dir = glob(realpath($directory) . DIRECTORY_SEPARATOR . '*');
foreach($dir as $file) {
if(is_dir($file)) {
$files = array_merge($files, collect_files($file));
continue;
}
$files[] = $file;
}
return $files;
}
echo 'Indexing changed files according to git...' . PHP_EOL;
$changed = git_changes();
echo 'Collecting files...' . PHP_EOL;
$sources = collect_files(__DIR__ . '/../src');
$tests = collect_files(__DIR__ . '/../tests');
$files = array_merge($sources, $tests);
$topDir = dirname(__DIR__) . DIRECTORY_SEPARATOR;
$now = date('Y-m-d');
foreach($files as $file) {
echo 'Scanning ' . str_replace($topDir, '', $file) . '...' . PHP_EOL;
try {
$handle = fopen($file, 'rb');
$checkPHP = trim(fgets($handle)) === '<?php';
if(!$checkPHP) {
echo 'File is not PHP.' . PHP_EOL;
continue;
}
$headerLines = [];
$expectLine = '// ' . basename($file);
$nameLine = trim(fgets($handle));
if($nameLine !== $expectLine) {
echo ' File name is missing or invalid, queuing update...' . PHP_EOL;
$headerLines['name'] = $expectLine;
}
$createdPrefix = '// Created: ';
$createdLine = trim(fgets($handle));
if(strpos($createdLine, $createdPrefix) !== 0) {
echo ' Creation date is missing, queuing update...' . PHP_EOL;
$headerLines['created'] = $createdPrefix . $now;
}
$updatedPrefix = '// Updated: ';
$updatedLine = trim(fgets($handle));
$updatedDate = substr($updatedLine, strlen($updatedPrefix));
if(strpos($updatedLine, $updatedPrefix) !== 0 || (in_array($file, $changed) && $updatedDate !== $now)) {
echo ' Updated date is inaccurate, queuing update...' . PHP_EOL;
$headerLines['updated'] = $updatedPrefix . $now;
}
$blankLine = trim(fgets($handle));
if(!empty($blankLine)) {
echo ' Trailing newline missing, queuing update...' . PHP_EOL;
$headerLines['blank'] = '';
}
if(!empty($headerLines)) {
fclose($handle);
try {
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ndx-uh-' . bin2hex(random_bytes(8)) . '.tmp';
copy($file, $tmpName);
$read = fopen($tmpName, 'rb');
$handle = fopen($file, 'wb');
fwrite($handle, fgets($read));
$insertAfter = [];
if(empty($headerLines['name']))
fwrite($handle, fgets($read));
else {
$line = fgets($read);
if(strpos($line, '// ') !== 0)
$insertAfter[] = $line;
fwrite($handle, $headerLines['name'] . "\n");
}
if(empty($headerLines['created']))
fwrite($handle, fgets($read));
else {
$line = fgets($read);
if(strpos($line, '// Created: ') !== 0)
$insertAfter[] = $line;
fwrite($handle, $headerLines['created'] . "\n");
}
if(empty($headerLines['updated']))
fwrite($handle, fgets($read));
else {
$line = fgets($read);
if(strpos($line, '// Updated: ') !== 0)
$insertAfter[] = $line;
fwrite($handle, $headerLines['updated'] . "\n");
}
if(!isset($headerLines['blank']))
fwrite($handle, fgets($read));
else {
$line = fgets($read);
if(!empty($line)) {
$insertAfter[] = $line;
fwrite($handle, "\n");
}
}
foreach($insertAfter as $line)
fwrite($handle, $line);
while(($line = fgets($read)) !== false)
fwrite($handle, $line);
} finally {
if(is_resource($read))
fclose($read);
if(is_file($tmpName))
unlink($tmpName);
}
}
} finally {
fclose($handle);
}
}