Completed
Push — master ( 0a0b74...dcdfcd )
by Woody
03:12
created

Generator::__construct()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 8
nop 3
dl 0
loc 25
ccs 5
cts 5
cp 1
crap 4
rs 8.5806
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace RoundingWell\Schematic;
5
6
use PhpParser\BuilderFactory;
7
use PhpParser\Node\Name;
8
use PhpParser\Node\Stmt\Class_ as Cls;
9
use PhpParser\Node\Stmt\Namespace_ as Ns;
10
use PhpParser\PrettyPrinterAbstract;
11
use PhpParser\PrettyPrinter\Standard as StandardPrinter;
12
use RoundingWell\Schematic\Schema\ObjectSchema;
13
14
class Generator
15
{
16
    /**
17
     * @var BuilderFactory
18
     */
19
    private $builder;
20
21
    /**
22
     * @var PrettyPrinterAbstract
23
     */
24
    private $printer;
25
26
    /**
27
     * @var Sysmte
28
     */
29
    private $system;
30
31 1
    public function __construct(
32
        BuilderFactory $factory = null,
33
        PrettyPrinterAbstract $printer = null,
34
        System $system = null
35
    ) {
36
        // @codeCoverageIgnoreStart
37
        if (empty($factory)) {
38
            $factory = new BuilderFactory();
39
        }
40
41
        if (empty($printer)) {
42
            $printer = new StandardPrinter([
43
                'shortArraySyntax' => true,
44
            ]);
45
        }
46
47
        if (empty($system)) {
48
            $system = new System();
49
        }
50
        // @codeCoverageIgnoreEnd
51
52 1
        $this->factory = $factory;
0 ignored issues
show
Bug introduced by
The property factory does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
53 1
        $this->printer = $printer;
54 1
        $this->system = $system;
0 ignored issues
show
Documentation Bug introduced by
It seems like $system of type object<RoundingWell\Schematic\System> is incompatible with the declared type object<RoundingWell\Schematic\Sysmte> of property $system.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
55 1
    }
56
57 1
    public function generate(ObjectSchema $schema, string $className, string $baseClass = ''): array
58
    {
59 1
        $classes = [];
60
61 1
        $className = new Name($className);
62
        // Start the class AST definition
63 1
        $namespace = $this->factory->namespace($className->slice(0, -1)->toString());
64 1
        $class = $this->factory->class($className->getLast());
65
66 1
        if ($baseClass) {
67 1
            $baseClassName = new Name($baseClass);
68
            // Import the base class with a "use" statement
69 1
            $namespace->addStmt($this->factory->use($baseClassName));
70
            // Make the class extend the base class
71 1
            $class->extend($baseClassName->getLast());
72
        }
73
74 1
        foreach ($schema->properties() as $name => $property) {
75 1
            $typeHint = $property->phpType();
76
77 1
            if ($property->isObject()) {
78
                // Create a new class for this property
79 1
                $nextClass = Name::concat($className, ucfirst($name));
80 1
                $typeHint = '\\' . $nextClass->toString();
81 1
                $classes = array_merge($classes, $this->generate(
82 1
                    $property,
0 ignored issues
show
Compatibility introduced by
$property of type object<RoundingWell\Schematic\Schema> is not a sub-type of object<RoundingWell\Sche...ic\Schema\ObjectSchema>. It seems like you assume a child class of the class RoundingWell\Schematic\Schema to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
83 1
                    $nextClass->toString(),
84 1
                    $baseClass
85
                ));
86 1
            } elseif ($property->isArray() && $property->items()->isObject()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class RoundingWell\Schematic\Schema as the method items() does only exist in the following sub-classes of RoundingWell\Schematic\Schema: RoundingWell\Schematic\Schema\ArraySchema. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
87
                // Create a new class for this array of properties
88 1
                $nextClass = Name::concat($className, ucfirst(singular($name)));
89 1
                $typeHint = '\\' . $nextClass->toString() . '[]';
90 1
                $classes = array_merge($classes, $this->generate(
91 1
                    $property->items(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class RoundingWell\Schematic\Schema as the method items() does only exist in the following sub-classes of RoundingWell\Schematic\Schema: RoundingWell\Schematic\Schema\ArraySchema. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
92 1
                    $nextClass->toString(),
93 1
                    $baseClass
94
                ));
95 1
            } elseif (!$schema->isRequired($name) && !$property->isNull()) {
96 1
                $typeHint = "$typeHint|null";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $typeHint instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
97
            }
98
99
            // Add a property declaration to the class
100 1
            $class->addStmt(
101 1
                $this->factory->property($name)
102 1
                    ->makePublic()
103 1
                    ->setDocComment("/**\n * @var $typeHint\n */")
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $typeHint instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
104
            );
105
        }
106
107
        // Add the class declaration to the namespace
108 1
        $namespace->addStmt($class);
109
110 1
        $classes[$className->toString()] = $namespace->getNode();
111
112 1
        return $classes;
113
    }
114
115
    /**
116
     * @param Ns[] $classes
117
     * @return string[]
118
     */
119 1
    public function write(array $classes, string $rootDirectory, string $rootNamespace = ''): array
120
    {
121 1
        $rootDirectory = rtrim($rootDirectory, '/');
122
123 1
        return array_map(
124 1
            function (Ns $node) use ($rootDirectory, $rootNamespace): string {
125
                // Remove the root (PSR-4) namespace and convert to a path
126 1
                $directory = str_replace($rootNamespace, '', $node->name->toString());
127 1
                $directory = trim(str_replace('\\', '/', $directory), '/');
128 1
                $directory = rtrim("$rootDirectory/$directory", '/');
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $rootDirectory instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $directory instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
129
                // Grab the class name from AST
130 1
                $class = $this->classNode($node->stmts)->name;
131
132 1
                $path = "$directory/$class.php";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $directory instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $class instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
133 1
                $code = $this->printer->prettyPrintFile([$node]);
134
135 1
                $this->system->writeFile($path, $code);
136
137 1
                return $path;
138 1
            },
139 1
            $classes
140
        );
141
    }
142
143 1
    private function classNode(array $stmts): Cls
144
    {
145 1
        foreach ($stmts as $stmt) {
146 1
            if ($stmt instanceof Cls) {
147 1
                return $stmt;
148
            }
149
        }
150
151
        // @codeCoverageIgnoreStart
152
        throw new \InvalidArgumentException(
153
            'Cannot find class node in statements'
154
        );
155
        // @codeCoverageIgnoreEnd
156
    }
157
}
158