Skip to content

Commit 3b4c253

Browse files
committed
restructured code
1 parent d6f0d09 commit 3b4c253

File tree

6 files changed

+526
-467
lines changed

6 files changed

+526
-467
lines changed

app/commands/MetaTester.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace App\Console;
44

5-
use App\Helpers\MetaFormats\AnnotationToAttributeConverter;
5+
use App\Helpers\MetaFormats\AnnotationConversion\AnnotationToAttributeConverter;
66
use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute;
77
use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat;
88
use App\Helpers\MetaFormats\FormatDefinitions\UserFormat;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\Helpers\MetaFormats\AnnotationConversion;
4+
5+
use App\Helpers\MetaFormats\Attributes\Param;
6+
use App\Helpers\MetaFormats\Type;
7+
8+
class AnnotationToAttributeConverter
9+
{
10+
public static function convertFile(string $path)
11+
{
12+
$content = StandardAnnotationConverter::preprocessFile($path);
13+
$nettePreprocess = NetteAnnotationConverter::regexReplaceAnnotations($content);
14+
15+
$netteCapturesList = $nettePreprocess["captures"];
16+
$contentWithPlaceholders = $nettePreprocess["contentWithPlaceholders"];
17+
18+
// move the attribute lines below the comment block
19+
$lines = [];
20+
$netteAttributeLinesCount = 0;
21+
$usingsAdded = false;
22+
$paramAttributeClass = Utils::shortenClass(Param::class);
23+
$paramTypeClass = Utils::shortenClass(Type::class);
24+
foreach (Utils::fileStringToLines($contentWithPlaceholders) as $line) {
25+
// detected the initial "use" block, add usings for new types
26+
if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") {
27+
$lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$paramAttributeClass};";
28+
$lines[] = "use App\\Helpers\\MetaFormats\\{$paramTypeClass};";
29+
foreach (Utils::getValidatorNames() as $validator) {
30+
$lines[] = "use App\\Helpers\\MetaFormats\\Validators\\{$validator};";
31+
}
32+
// write the detected line (the first detected "use" line)
33+
$lines[] = $line;
34+
$usingsAdded = true;
35+
// detected an attribute line placeholder, increment the counter and remove the line
36+
} elseif (str_contains($line, NetteAnnotationConverter::$netteAttributePlaceholder)) {
37+
$netteAttributeLinesCount++;
38+
// detected the end of the comment block "*/", flush attribute lines
39+
} elseif (trim($line) === "*/") {
40+
$lines[] = $line;
41+
for ($i = 0; $i < $netteAttributeLinesCount; $i++) {
42+
$annotationParameters = NetteAnnotationConverter::convertNetteRegexCapturesToDictionary($netteCapturesList[$i]);
43+
$parenthesesBuilder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($annotationParameters);
44+
$attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]";
45+
// change to multiline if the line is too long
46+
if (strlen($attributeLine) > 120) {
47+
$attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toMultilineString(4)}]";
48+
}
49+
$lines[] = $attributeLine;
50+
}
51+
52+
// remove the captures used in this endpoint
53+
$netteCapturesList = array_slice($netteCapturesList, $netteAttributeLinesCount);
54+
// reset the counters for the next detected endpoint
55+
$netteAttributeLinesCount = 0;
56+
} else {
57+
$lines[] = $line;
58+
}
59+
}
60+
61+
return Utils::linesToFileString($lines);
62+
}
63+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<?php
2+
3+
namespace App\Helpers\MetaFormats\AnnotationConversion;
4+
5+
use App\Exceptions\InternalServerException;
6+
use App\Helpers\MetaFormats\Type;
7+
use App\Helpers\MetaFormats\Validators\VArray;
8+
use App\Helpers\MetaFormats\Validators\VBool;
9+
use App\Helpers\MetaFormats\Validators\VEmail;
10+
use App\Helpers\MetaFormats\Validators\VFloat;
11+
use App\Helpers\MetaFormats\Validators\VInt;
12+
use App\Helpers\MetaFormats\Validators\VString;
13+
use App\Helpers\MetaFormats\Validators\VTimestamp;
14+
use App\Helpers\MetaFormats\Validators\VUuid;
15+
use App\Helpers\Swagger\ParenthesesBuilder;
16+
17+
class NetteAnnotationConverter
18+
{
19+
/**
20+
* A regex that matches @Param annotations and captures its parameters. Can capture up to 7 parameters.
21+
* Contains 6 copies of the following sub-regex: '(?:([a-z]+?=.+?),?\s*\*?\s*)?', which
22+
* matches 'name=value' assignments followed by an optional comma, whitespace,
23+
* star (multi-line annotation support), whitespace. The capture contains only 'name=value'.
24+
* The regex ends with '([a-z]+?=.+)\)', which is similar to the above, but instead of ending with
25+
* an optional comma etc., it ends with the closing parentheses of the @Param annotation.
26+
*/
27+
private static string $netteRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/";
28+
29+
// placeholder for detected nette annotations ("@Param")
30+
// this text must not be present in the presenter files
31+
public static string $netteAttributePlaceholder = "<!>#nette#<!>";
32+
33+
public static function regexReplaceAnnotations(string $fileContent)
34+
{
35+
// Array that contains parentheses builders of all future generated attributes.
36+
// Filled dynamically with the preg_replace_callback callback.
37+
$netteCapturesList = [];
38+
39+
$withPlaceholders = preg_replace_callback(
40+
self::$netteRegex,
41+
function ($matches) use (&$netteCapturesList) {
42+
return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList);
43+
},
44+
$fileContent,
45+
flags: PREG_UNMATCHED_AS_NULL
46+
);
47+
48+
return [
49+
"contentWithPlaceholders" => $withPlaceholders,
50+
"captures" => $netteCapturesList,
51+
];
52+
}
53+
54+
public static function convertNetteRegexCapturesToDictionary(array $captures)
55+
{
56+
// convert the string assignments in $captures to an associative array
57+
$annotationParameters = [];
58+
// the first element is the matched string
59+
for ($i = 1; $i < count($captures); $i++) {
60+
$capture = $captures[$i];
61+
if ($capture === null) {
62+
continue;
63+
}
64+
65+
// the regex extracts the key as the first capture, and the value as the second or third (depends
66+
// whether the value is enclosed in double quotes)
67+
$parseResult = preg_match('/([a-z]+)=(?:(?:"(.+?)")|(?:(.+)))/', $capture, $tokens, PREG_UNMATCHED_AS_NULL);
68+
if ($parseResult !== 1) {
69+
throw new InternalServerException("Unexpected assignment format: $capture");
70+
}
71+
72+
$key = $tokens[1];
73+
$value = $tokens[2] ?? $tokens[3];
74+
$annotationParameters[$key] = $value;
75+
}
76+
77+
return $annotationParameters;
78+
}
79+
80+
/**
81+
* Used by preg_replace_callback to replace "@Param" annotation captures with placeholder strings to mark the
82+
* lines for future replacement. Additionally stores the captures into an output array.
83+
* @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag).
84+
* @param array $capturesList An output list for captures.
85+
* @return string Returns a placeholder.
86+
*/
87+
private static function netteRegexCaptureToAttributeCallback(array $captures, array &$capturesList)
88+
{
89+
$capturesList[] = $captures;
90+
return self::$netteAttributePlaceholder;
91+
}
92+
93+
/**
94+
* @return string[] Returns an array of Validator class names (without the namespace).
95+
*/
96+
private static function getValidatorNames()
97+
{
98+
$dir = __DIR__ . "/Validators";
99+
$baseFilenames = scandir($dir);
100+
$classNames = [];
101+
foreach ($baseFilenames as $filename) {
102+
if (!str_ends_with($filename, ".php")) {
103+
continue;
104+
}
105+
106+
// remove the ".php" suffix
107+
$className = substr($filename, 0, -4);
108+
$classNames[] = $className;
109+
}
110+
return $classNames;
111+
}
112+
113+
/**
114+
* Converts annotation validation values (such as "string:1..255") to Validator construction
115+
* strings (such as "new VString(1, 255)").
116+
* @param string $validation The annotation validation string.
117+
* @return string Returns the object construction string.
118+
*/
119+
private static function convertAnnotationValidationToValidatorString(string $validation): string
120+
{
121+
if (str_starts_with($validation, "string")) {
122+
$stringValidator = Utils::shortenClass(VString::class);
123+
124+
// handle string length constraints, such as "string:1..255"
125+
if (strlen($validation) > 6) {
126+
if ($validation[6] !== ":") {
127+
throw new InternalServerException("Unknown string validation format: $validation");
128+
}
129+
$suffix = substr($validation, 7);
130+
131+
// special case for uuids
132+
if ($suffix === "36") {
133+
return "new " . Utils::shortenClass(VUuid::class) . "()";
134+
}
135+
136+
// capture the two bounding numbers and the double dot in strings of
137+
// types "1..255", "..255", "1..", or "255"
138+
if (preg_match("/([0-9]*)(..)?([0-9]+)?/", $suffix, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
139+
throw new InternalServerException("Unknown string validation format: $validation");
140+
}
141+
142+
// type "255", exact match
143+
if ($matches[2] === null) {
144+
return "new {$stringValidator}({$matches[1]}, {$matches[1]})";
145+
// type "1..255"
146+
} elseif ($matches[1] !== null && $matches[3] !== null) {
147+
return "new {$stringValidator}({$matches[1]}, {$matches[3]})";
148+
// type "..255"
149+
} elseif ($matches[1] === null) {
150+
return "new {$stringValidator}(0, {$matches[3]})";
151+
// type "1.."
152+
} elseif ($matches[3] === null) {
153+
return "new {$stringValidator}({$matches[1]})";
154+
}
155+
156+
throw new InternalServerException("Unknown string validation format: $validation");
157+
}
158+
159+
return "new {$stringValidator}()";
160+
}
161+
162+
// non-string validation rules do not have parameters, so they can be converted directly
163+
$validatorClass = null;
164+
switch ($validation) {
165+
case "email":
166+
// there is one occurrence of this
167+
case "email:1..":
168+
$validatorClass = VEmail::class;
169+
break;
170+
case "numericint":
171+
case "integer":
172+
$validatorClass = VInt::class;
173+
break;
174+
case "bool":
175+
case "boolean":
176+
$validatorClass = VBool::class;
177+
break;
178+
case "array":
179+
case "list":
180+
$validatorClass = VArray::class;
181+
break;
182+
case "timestamp":
183+
$validatorClass = VTimestamp::class;
184+
break;
185+
case "numeric":
186+
$validatorClass = VFloat::class;
187+
break;
188+
default:
189+
throw new InternalServerException("Unknown validation rule: $validation");
190+
}
191+
192+
return "new " . Utils::shortenClass($validatorClass) . "()";
193+
}
194+
195+
/**
196+
* Convers an associative array into an attribute string builder.
197+
* @param array $annotationParameters An associative array with a subset of the following keys:
198+
* type, name, validation, description, required, nullable.
199+
* @throws \App\Exceptions\InternalServerException
200+
* @return ParenthesesBuilder A string builder used to build the final attribute string.
201+
*/
202+
public static function convertRegexCapturesToParenthesesBuilder(array $annotationParameters)
203+
{
204+
// serialize the parameters to an attribute
205+
$parenthesesBuilder = new ParenthesesBuilder();
206+
207+
// add type
208+
if (!array_key_exists("type", $annotationParameters)) {
209+
throw new InternalServerException("Missing type parameter.");
210+
}
211+
212+
$typeStr = $annotationParameters["type"];
213+
$paramTypeClass = Utils::shortenClass(Type::class);
214+
$type = null;
215+
switch ($typeStr) {
216+
case "post":
217+
$type = $paramTypeClass . "::Post";
218+
break;
219+
case "query":
220+
$type = $paramTypeClass . "::Query";
221+
break;
222+
case "path":
223+
$type = $paramTypeClass . "::Path";
224+
break;
225+
default:
226+
throw new InternalServerException("Unknown request type: $typeStr");
227+
}
228+
$parenthesesBuilder->addValue($type);
229+
230+
// add name
231+
if (!array_key_exists("name", $annotationParameters)) {
232+
throw new InternalServerException("Missing name parameter.");
233+
}
234+
$parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\"");
235+
236+
$nullable = false;
237+
if (array_key_exists("validation", $annotationParameters)) {
238+
$validation = $annotationParameters["validation"];
239+
240+
if (Utils::checkValidationNullability($validation)) {
241+
// remove the '|null' from the end of the string
242+
$validation = substr($validation, 0, -5);
243+
$nullable = true;
244+
}
245+
246+
// this will always produce a single validator (the annotations do not contain multiple validation fields)
247+
$validator = self::convertAnnotationValidationToValidatorString($validation);
248+
$parenthesesBuilder->addValue(value: "[ $validator ]");
249+
}
250+
251+
if (array_key_exists("description", $annotationParameters)) {
252+
$parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\"");
253+
}
254+
255+
if (array_key_exists("required", $annotationParameters)) {
256+
$parenthesesBuilder->addValue("required: " . $annotationParameters["required"]);
257+
}
258+
259+
if ($nullable) {
260+
$parenthesesBuilder->addValue("nullable: true");
261+
}
262+
263+
return $parenthesesBuilder;
264+
}
265+
}

0 commit comments

Comments
 (0)