Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@
"league/mime-type-detection": "^1.16",
"league/oauth2-client": "^2.8",
"monolog/monolog": "^3.7.0",
"nette/php-generator": "4.2.3",
"nette/schema": "^1.3.4",
"nikic/php-parser": "^5.3",
"nikic/php-parser": "^5.9",
"php": "^8.5",
"psr-discovery/http-client-implementations": "^1.4",
"psr-discovery/http-factory-implementations": "^1.2",
Expand Down
1 change: 0 additions & 1 deletion docs/5-extra-topics/01-package-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ The user will have a chance to specify the destination of the file, and whether
use Tempest\Core\Installer;
use Tempest\Core\PublishesFiles;
use Tempest\Discovery\SkipDiscovery;
use Tempest\Generation\ClassManipulator;
use function Tempest\src_namespace;
use function Tempest\src_path;

Expand Down
4 changes: 1 addition & 3 deletions packages/generation/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
"minimum-stability": "dev",
"require": {
"php": "^8.5",
"nette/php-generator": "4.2.3",
"nette/schema": "^1.3.4",
"nikic/php-parser": "^5.3",
"nikic/php-parser": "^5.9",
"tempest/support": "3.x-dev",
"psr/container": "^2.0"
},
Expand Down
51 changes: 51 additions & 0 deletions packages/generation/src/Php/Attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\Php;

use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt;

/**
* @internal
*/
final class Attributes
{
/**
* @param array<array-key, mixed> $arguments
*/
public static function add(Stmt\Class_|Stmt\ClassMethod|Param $node, string $name, array $arguments = []): void
{
$node->attrGroups[] = new AttributeGroup([
new Attribute(new Name\FullyQualified(ltrim($name, '\\')), ValueExporter::exportArguments($arguments)),
]);
}

public static function remove(Stmt\Class_|Stmt\ClassMethod|Param $node, string $name): void
{
$groups = [];

foreach ($node->attrGroups as $group) {
$attributes = array_values(array_filter(
$group->attrs,
static fn (Attribute $attribute) => ! Names::refersTo($attribute->name, $name),
));

if ($attributes === []) {
continue;
}

if (count($attributes) !== count($group->attrs)) {
$group->attrs = $attributes;
}

$groups[] = $group;
}

$node->attrGroups = $groups;
}
}
86 changes: 86 additions & 0 deletions packages/generation/src/Php/Changes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\Php;

use PhpParser\Node;
use PhpParser\Node\Stmt;

/**
* Tells which nodes of loaded code are identical to the node they were cloned from,
* so the printer can copy their original code instead of reconstructing it.
*
* @internal
*/
final class Changes
{
private const string CHANGED_ATTRIBUTE = 'tempest.changed';

/**
* The node types that `PhpFile`, `PhpClass` and `PhpMethod` change in place. They are only ever nested in each other,
* so any other node can only change when a name inside it is replaced, which marks it as changed.
*/
private const array MUTABLE_NODES = [
Stmt\Namespace_::class => true,
Stmt\Use_::class => true,
Stmt\GroupUse::class => true,
Stmt\Class_::class => true,
Stmt\ClassMethod::class => true,
Stmt\Declare_::class => true,
Node\DeclareItem::class => true,
Node\AttributeGroup::class => true,
Node\Param::class => true,
Node\UseItem::class => true,
];

/**
* Marks a node as changed, returning whether it wasn't marked yet.
*/
public static function mark(Node $node): bool
{
if ($node->getAttribute(self::CHANGED_ATTRIBUTE) === true) {
return false;
}

$node->setAttribute(self::CHANGED_ATTRIBUTE, true);

return true;
}

public static function isUnchanged(Node $node): bool
{
$original = $node->getAttribute('origNode');

if (! $original instanceof Node || $node->getAttribute(self::CHANGED_ATTRIBUTE) === true) {
return false;
}

if (! isset(self::MUTABLE_NODES[$node::class])) {
return true;
}

if ($node->getComments() !== $original->getComments()) {
return false;
}

return array_all($node->getSubNodeNames(), static fn (string $name) => self::isSame($node->{$name}, $original->{$name}));
}

private static function isSame(mixed $value, mixed $original): bool
{
if ($value instanceof Node) {
return $value->getAttribute('origNode') === $original && self::isUnchanged($value);
}

if (! is_array($value)) {
return $value === $original;
}

if (! is_array($original) || count($value) !== count($original)) {
return false;
}

return array_all($value, static fn (mixed $item, int|string $key) => array_key_exists($key, $original) && self::isSame($item, $original[$key]));
}
}
8 changes: 2 additions & 6 deletions packages/generation/src/Php/ClassGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

namespace Tempest\Generation\Php;

use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\PhpFile;

final class ClassGenerator
{
use ManipulatesPhpClasses;

public function __construct(string $name, string $namespace)
{
$this->file = new PhpFile();
$this->classType = new ClassType($name);
$this->namespace = $namespace;
$this->file = PhpFile::create($namespace);
$this->class = $this->file->addClass($name);
}
}
38 changes: 21 additions & 17 deletions packages/generation/src/Php/ClassManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Tempest\Generation\Php;

use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\PhpFile;
use ReflectionClass;
use Tempest\Generation\Php\Exceptions\FileGenerationFailedException;
use Tempest\Support\Filesystem;
Expand All @@ -15,18 +13,29 @@ final class ClassManipulator
{
use ManipulatesPhpClasses;

public function __construct(string|ReflectionClass $source)
/**
* @param string|class-string|ReflectionClass|PhpFile $source a file path, a class name, or the loaded file
*/
public function __construct(string|ReflectionClass|PhpFile $source)
{
if (is_file($source)) {
$this->classType = ClassType::fromCode(Filesystem\read_file($source));
} elseif (is_string($source)) {
$this->classType = ClassType::from($source, withBodies: true);
} else {
$this->classType = ClassType::from($source->getName(), withBodies: true);
if ($source instanceof PhpFile) {
$this->file = $source;
$this->class = $source->getClass();

return;
}

if (is_string($source) && is_file($source)) {
$this->file = PhpFile::fromPath($source);
$this->class = $this->file->getClass();

return;
}

$this->file = new PhpFile();
$this->namespace = $this->classType->getNamespace()->getName();
$reflection = is_string($source) ? new ReflectionClass($source) : $source;

$this->file = PhpFile::fromPath($reflection->getFileName());
$this->class = $this->file->getClass($reflection->getShortName());
}

/**
Expand All @@ -38,13 +47,8 @@ public function __construct(string|ReflectionClass $source)
*/
public function save(string $path): self
{
$dir = pathinfo($path, PATHINFO_DIRNAME);

if (! is_dir($dir)) {
mkdir($dir, recursive: true);
}

try {
Filesystem\create_directory_for_file($path);
Filesystem\write_file($path, $this->print());
} catch (FilesystemException) {
throw new FileGenerationFailedException(sprintf('The file "%s" could not be written.', $path));
Expand Down
Loading
Loading