Skip to content
Open
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: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"justinrainbow/json-schema": "^6.0",
"ext-json": "*",
"composer/semver": "^3.0",
"laminas/laminas-code": "^4.12"
"laminas/laminas-code": "^4.12",
"nikic/php-parser": "^4.19"
},
"autoload": {
"psr-4": {
Expand Down
116 changes: 116 additions & 0 deletions src/Codegen/AbstractGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace Helmich\Schema2Class\Codegen;

use PhpParser\Node;

abstract class AbstractGenerator
{
/**
* @param Node[] $nodes
*/
public function __construct(protected array $nodes)
{
}

public function insert(int $index, Node $node): self
{
array_splice($this->nodes, $index, 0, [$node]);

return $this;
}

public function set(int $index, $value): self
{
if (!$this->isValidIndex($index)) {
throw new \OutOfRangeException();
}

$this->nodes[$index] = $value;

return $this;
}

public function remove(int $index): Node
{
if (!$this->isValidIndex($index)) {
throw new \OutOfRangeException();
}

return array_splice($this->nodes, $index, 1)[0];
}

public function clear(): self
{
$this->nodes = [];

return $this;
}

/**
* @psalm-param callable(Node): Node $callback
*/
public function walk(callable $callback): self
{
array_walk($this->nodes, $callback);

return $this;
}

/**
* @psalm-param ($mode is 0 ? callable(Node) : callable(Node, int) ) $callback
*/
public function filter(callable $callback, int $mode = 0): self
{
$this->nodes = array_filter($this->nodes, $callback, $mode);
$this->nodes = array_values($this->nodes);

return $this;
}

public function find(Node $value): ?int
{
$offset = array_search($value, $this->nodes, true);

return $offset === false ? null : $offset;
}

public function first(): Node
{
if (empty($this->nodes)) {
throw new \UnderflowException();
}

return $this->nodes[0];
}

public function get(int $index): Node
{
if (!$this->isValidIndex($index)) {
throw new \OutOfRangeException();
}

return $this->nodes[$index];
}

public function last(): Node
{
if (empty($this->nodes)) {
throw new \UnderflowException();
}

return $this->nodes[array_key_last($this->nodes)];
}

public function count(): int
{
return count($this->nodes);
}

protected function isValidIndex(int $index): bool
{
return array_key_exists($index, $this->nodes);
}
}
82 changes: 82 additions & 0 deletions src/Codegen/EnumGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Helmich\Schema2Class\Codegen;

use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\DeclareDeclare;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\EnumCase;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Nop;
use PhpParser\PrettyPrinter\Standard;

class EnumGenerator extends AbstractGenerator
{
/** @var array<string, EnumCase> */
protected array $cases = [];

public function __construct(
protected Enum_ $enum_,
protected Namespace_ $namespace_
)
{
parent::__construct([
$this->buildStrictTypes(),
new Nop(),
$this->namespace_,
$this->enum_,
]);
}

public function withEnum_(Enum_ $enum_): self
{
foreach ($this->cases as $name => $case) {
$enum_->stmts[$name] = $case;
}

$currentEnumIndex = $this->find($this->enum_);
if ($currentEnumIndex !== null) {
$this->set($currentEnumIndex, $enum_);
}

$this->enum_ = $enum_;

return $this;
}

public function withNamespace_(Namespace_ $namespace_): self
{
$currentNamespaceIndex = $this->find($this->namespace_);
if ($currentNamespaceIndex !== null) {
$this->set($currentNamespaceIndex, $namespace_);
}

$this->namespace_ = $namespace_;

return $this;
}

public function withAdditionalEnumCase(EnumCase $enumCase): self
{
$this->cases[$enumCase->name->toLowerString()] = $enumCase;
$this->enum_->stmts[$enumCase->name->toString()] = $enumCase;

return $this;
}

protected function buildStrictTypes(): Declare_
{
$declareDeclare = new DeclareDeclare(new Identifier('strict_types'), new LNumber(1));
return new Declare_([$declareDeclare]);
}

public function generate(): string
{
$prettyPrinter = new Standard();
return $prettyPrinter->prettyPrint($this->nodes);
}
}
2 changes: 1 addition & 1 deletion src/Generator/GeneratorHookRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Helmich\Schema2Class\Generator;

use Helmich\Schema2Class\Codegen\EnumGenerator;
use Laminas\Code\Generator\ClassGenerator;
use Laminas\Code\Generator\EnumGenerator\EnumGenerator;
use Laminas\Code\Generator\FileGenerator;

trait GeneratorHookRunner
Expand Down
2 changes: 1 addition & 1 deletion src/Generator/Hook/EnumCreatedHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Helmich\Schema2Class\Generator\Hook;

use Laminas\Code\Generator\EnumGenerator\EnumGenerator;
use Helmich\Schema2Class\Codegen\EnumGenerator;

/**
* Interface definition for hooks that are called when an enumeration is created.
Expand Down
1 change: 0 additions & 1 deletion src/Generator/SchemaToClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Laminas\Code\Generator\ClassGenerator;
use Laminas\Code\Generator\DocBlock\Tag\GenericTag;
use Laminas\Code\Generator\DocBlockGenerator;
use Laminas\Code\Generator\EnumGenerator\EnumGenerator;
use Laminas\Code\Generator\FileGenerator;
use Symfony\Component\Console\Output\OutputInterface;

Expand Down
26 changes: 13 additions & 13 deletions src/Generator/SchemaToEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Helmich\Schema2Class\Generator;

use Helmich\Schema2Class\Codegen\EnumGenerator;
use Helmich\Schema2Class\Writer\WriterInterface;
use Laminas\Code\DeclareStatement;
use Laminas\Code\Generator\EnumGenerator\EnumGenerator;
use Laminas\Code\Generator\FileGenerator;
use PhpParser\Builder\Enum_;
use PhpParser\Builder\EnumCase;
use PhpParser\Builder\Namespace_;

class SchemaToEnum
{
Expand Down Expand Up @@ -39,24 +41,22 @@ public function schemaToEnum(GeneratorRequest $req): void

$type = $req->getSchema()["type"] === "string" ? "string" : "int";
$enumName = $req->getTargetNamespace() . "\\" . $req->getTargetClass();
$enum = EnumGenerator::withConfig([
"name" => $enumName,
"backedCases" => [
"type" => $type,
"cases" => $cases,
],
]);
$enumGenerator = new EnumGenerator(
enum_: (new Enum_($req->getTargetClass()))->setScalarType($type)->getNode(),
namespace_: (new Namespace_($req->getTargetNamespace()))->getNode(),
);
foreach ($cases as $name => $value) {
$enumGenerator->withAdditionalEnumCase((new EnumCase($name))->setValue($value)->getNode());
}

$req->onEnumCreated($enumName, $enum);
$req->onEnumCreated($enumName, $enumGenerator);

$filename = $req->getTargetDirectory() . '/' . $req->getTargetClass() . '.php';
$file = new FileGenerator();
$file->setBody($enum->generate());
$file->setBody($enumGenerator->generate());

$req->onFileCreated($filename, $file);

$file->setDeclares([DeclareStatement::strictTypes(1)]);

$content = $file->generate();

// Do some corrections because the Zend code generation library is stupid.
Expand Down
96 changes: 96 additions & 0 deletions tests/Codegen/AbstractGeneratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Helmich\Schema2Class\Codegen;

use PhpParser\Builder\Enum_;
use PhpParser\Builder\Namespace_;
use PhpParser\Builder\Use_;
use PhpParser\Node;
use PHPUnit\Framework\TestCase;

final class AbstractGeneratorTest extends TestCase
{
public function test_last_throws_exception_on_empty_nodes(): void
{
$generator = new class([]) extends AbstractGenerator {};

$this->expectException(\UnderflowException::class);

$generator->last();
}

public function test_first_throws_exception_on_empty_nodes(): void
{
$generator = new class([]) extends AbstractGenerator {};

$this->expectException(\UnderflowException::class);

$generator->last();
}

public function test_remove_and_set(): void
{
$firstNode = (new Namespace_('TestNamespace'))->getNode();
$secondNode = (new Use_('TestImport', Node\Stmt\Use_::TYPE_NORMAL))->getNode();
$thirdNode = (new Enum_('TestEnum'))->getNode();
$setNode = (new Enum_('SetEnum'))->getNode();

$generator = new class([$firstNode, $secondNode, $thirdNode]) extends AbstractGenerator {};
$removed = $generator->remove(1);
$generator->set(1, $setNode);

$this->assertSame($secondNode, $removed);
$this->assertSame(2, $generator->count());
$this->assertSame($firstNode, $generator->first());
$this->assertSame($firstNode, $generator->get(0));
$this->assertSame($setNode, $generator->last());
}

public function test_insert(): void
{
$firstNode = (new Namespace_('TestNamespaceInsert'))->getNode();
$secondNode = (new Use_('TestImportInsert', Node\Stmt\Use_::TYPE_NORMAL))->getNode();
$insertNode = (new Enum_('InsertedEnum'))->getNode();

$generator = new class([$firstNode, $secondNode]) extends AbstractGenerator {};
$generator->insert(1, $insertNode);

$this->assertSame(3, $generator->count());
$this->assertSame($firstNode, $generator->first());
$this->assertSame($insertNode, $generator->get(1));
$this->assertSame($secondNode, $generator->last());
}

public function test_filter(): void
{
$firstNode = (new Namespace_('TestNamespaceInsert'))->getNode();
$secondNode = (new Use_('TestImportInsert', Node\Stmt\Use_::TYPE_NORMAL))->getNode();
$thirdNode = (new Enum_('InsertedEnum'))->getNode();

$generator = new class([$firstNode, $secondNode, $thirdNode]) extends AbstractGenerator {};
$generator->filter(function (Node $value) {
return $value instanceof Node\Stmt\Enum_;
});

$this->assertSame(1, $generator->count());
$this->assertSame($thirdNode, $generator->first());
}

public function test_walk(): void
{
$firstNode = (new Namespace_('TestNamespaceInsert'))->getNode();
$secondNode = (new Use_('TestImportInsert', Node\Stmt\Use_::TYPE_NORMAL))->getNode();
$replaceNode = (new Enum_('InsertedEnum'))->getNode();

$generator = new class([$firstNode, $secondNode]) extends AbstractGenerator {};
$generator->walk(function (Node &$value) use ($replaceNode) {
$value = $replaceNode;
});

$this->assertSame(2, $generator->count());
$this->assertSame($replaceNode, $generator->first());
$this->assertSame($replaceNode, $generator->last());
}
}
5 changes: 3 additions & 2 deletions tests/Generator/Fixtures/Enum/Output/Foo.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<?php

declare(strict_types=1);
declare (strict_types=1);

namespace Ns\Enum;

enum Foo: string {
enum Foo : string
{
case Foo = 'Foo';
case Bar = 'Bar';
case Baz = 'Baz';
Expand Down
Loading