Issues (3)

src/functions.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Zlikavac32\Enum;
6
7
use LogicException;
8
use ReflectionClass;
9
use ReflectionException;
10
use function get_class;
11
use function gettype;
12
use function is_object;
13
use function is_string;
14
use function is_subclass_of;
15
use function preg_match;
16
use function sprintf;
17
18
/**
19
 * @throws LogicException If value in $fqn is not a class extending Zlikavac32\Enum\Enum
20
 */
21
function assertFqnIsEnumClass(string $fqn): void
22
{
23
    if (is_subclass_of($fqn, Enum::class)) {
24
        return ;
25
    }
26
27
    throw new LogicException(sprintf('%s does not have %s as it\'s parent', $fqn, Enum::class));
28
}
29
30
/**
31
 * Checks for library specific constraints put on an enum class.
32
 *
33
 * Currently they are:
34
 *   - enum class must be abstract
35
 *   - no class between enum class and Zlikavac32\Enum\Enum (both exclusive) can implement enumerate() method
36
 *   - every class in the chain must be abstract
37
 *
38
 * @throws ReflectionException If something went wrong in reflection API
39
 * @throws LogicException If some condition is not fulfilled
40
 */
41
function assertEnumClassAdheresConstraints(string $fqn): void {
42
    assertEnumClassIsAbstract($fqn);
43
    assertEnumClassParentsAdhereConstraints($fqn);
44
    assertNoParentHasPHPDocMethodForClass($fqn);
45
}
46
47
/**
48
 * Functions asserts that parents (except for Zlikavac32\Enum\Enum) are abstract and do not define enumerate() method.
49
 *
50
 * @throws ReflectionException If something went wrong in reflection API
51
 * @throws LogicException If one of parents is not abstract
52
 * @throws LogicException If one of parents defines enumerate() method
53
 */
54
function assertEnumClassParentsAdhereConstraints(string $fqn): void {
55
    foreach (class_parents($fqn) as $parent) {
56
        $reflectionClass = new ReflectionClass($parent);
57
        $reflectionMethod = $reflectionClass->getMethod('enumerate');
58
59
        $declaringClass = $reflectionMethod->getDeclaringClass();
60
61
        if ($declaringClass->name !== Enum::class) {
62
            throw new LogicException(
63
                sprintf('Enum %s extends %s which already defines enumerate() method', $fqn, $parent)
64
            );
65
        }
66
67
        assertEnumClassIsAbstract($parent);
68
    }
69
}
70
71
function assertNoParentHasPHPDocMethodForClass(string $fqn): void {
72
    foreach (class_parents($fqn) as $parent) {
73
        $reflectionClass = new ReflectionClass($parent);
74
75
        if (
76
            $reflectionClass->getDocComment()
77
            &&
78
            preg_match('/@method\s+static/', $reflectionClass->getDocComment())
0 ignored issues
show
It seems like $reflectionClass->getDocComment() can also be of type true; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

78
            preg_match('/@method\s+static/', /** @scrutinizer ignore-type */ $reflectionClass->getDocComment())
Loading history...
79
        ) {
80
            throw new LogicException(
81
                sprintf('Enum %s extends %s which already defines enum names in PHPDoc', $fqn, $parent)
82
            );
83
        }
84
    }
85
}
86
87
/**
88
 * @throws LogicException If pattern "/^[a-zA-Z_][a-zA-Z_0-9]*$/" is not satisfied
89
 */
90
function assertValidNamePattern(string $name): void
91
{
92
    $pattern = '/^[a-zA-Z_][a-zA-Z_0-9]*$/';
93
94
    if (preg_match($pattern, $name)) {
95
        return ;
96
    }
97
98
    throw new LogicException(sprintf('Element name "%s" does not match pattern %s', $name, $pattern));
99
}
100
101
/**
102
 * Checks that enum class is abstract.
103
 *
104
 * @throws ReflectionException If something went wrong in reflection API
105
 * @throws LogicException If enum class is not abstract
106
 */
107
function assertEnumClassIsAbstract(string $fqn): void
108
{
109
    if ((new ReflectionClass($fqn))->isAbstract()) {
110
        return ;
111
    }
112
113
    throw new LogicException(sprintf('Enum %s must be declared as abstract', $fqn));
114
}
115
116
/**
117
 * Checks whether a collection of enum objects represents valid collection for enum class in $class. Parameter
118
 * $enumClassFqn is same as $class and will be removed later.
119
 *
120
 * Rules that must be satisfied:
121
 *   - element name must be valid string
122
 *   - enum object must be instance of $class
123
 *
124
 * @throws LogicException If some condition is not fulfilled
125
 */
126
function assertValidEnumCollection(string $class, array $enumCollection, string $enumClassFqn): void
127
{
128
    foreach ($enumCollection as $elementName => $object) {
129
        assertElementNameIsString($class, $elementName);
130
        assertValidNamePattern($elementName);
131
        assertValidEnumElementObjectType($class, $object, $enumClassFqn);
132
    }
133
}
134
135
/**
136
 * Asserts that value in $name is string.
137
 *
138
 * @param $name
139
 *
140
 * @throws LogicException If value in $name is not string
141
 */
142
function assertElementNameIsString(string $class, $name): void
143
{
144
    if (is_string($name)) {
145
        return;
146
    }
147
148
    if (is_object($name)) {
149
        $formattedElementName = sprintf('(object instance of %s)', get_class($name));
150
    } else {
151
        $formattedElementName = $name;
152
    }
153
154
    throw new LogicException(
155
        sprintf('Element name %s in enum %s is not valid', $formattedElementName, $class)
156
    );
157
}
158
159
/**
160
 * Asserts that $object is instance off $class. Parameter $enumClassFqn must (although is not enforced currently) have
161
 * same value as $class. It will be removed in the future.
162
 *
163
 * @param mixed $object
164
 *
165
 * @throws LogicException If $object is not an instance of $class
166
 */
167
function assertValidEnumElementObjectType(string $class, $object, string $enumClassFqn): void
168
{
169
    if ($object instanceof $class) {
170
        return;
171
    }
172
173
    $resolvedType = gettype($object);
174
175
    if ('object' === $resolvedType) {
176
        $resolvedType = 'an instance of ' . get_class($object);
177
    }
178
179
    throw new LogicException(
180
        sprintf(
181
            'Enum element object in enum %s must be an instance of %s (%s received)',
182
            $enumClassFqn,
183
            $class,
184
            $resolvedType
185
        )
186
    );
187
}
188