diff --git a/.gitignore b/.gitignore
index f6bf53da3..3426cdf85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,4 @@ coverage.html
/run_server.bat
/*.bat
/filestorage
+app/V1Module/presenters/_autogenerated_annotations_temp.php
diff --git a/app/commands/GenerateSwagger.php b/app/commands/GenerateSwagger.php
index f75b6c457..348d3f82e 100644
--- a/app/commands/GenerateSwagger.php
+++ b/app/commands/GenerateSwagger.php
@@ -2,626 +2,43 @@
namespace App\Console;
-use App\Helpers\ApiConfig;
-use App\V1Module\Router\MethodRoute;
-use Doctrine\DBAL\Connection;
-use Doctrine\ORM\EntityManager;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Tools\SchemaTool;
-use JsonSerializable;
-use Nette\Application\IPresenterFactory;
-use Nette\Application\Routers\RouteList;
-use Nette\Application\UI\Presenter;
-// use Nette\Reflection\ClassType;
-// use Nette\Reflection\IAnnotation;
-// use Nette\Reflection\Method;
-use Nette\Utils\ArrayHash;
-use Nette\Utils\Arrays;
-use Nette\Utils\Finder;
-use Nette\Utils\Json;
-use Nette\Utils\Strings;
-use ReflectionClass;
-use ReflectionException;
-use SplFileInfo;
use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use App\Helpers\Yaml;
-use Zenify\DoctrineFixtures\Alice\AliceLoader;
+use OpenApi\Generator;
-// Completely removed -- needs rewriting for OpenAPI specs
-// class GenerateSwagger extends Command
-// {
-// protected static $defaultName = 'swagger:generate';
-//
-// /**
-// * @var RouteList
-// */
-// private $router;
-//
-// /**
-// * @var IPresenterFactory
-// */
-// private $presenterFactory;
-//
-// /**
-// * @var AliceLoader
-// */
-// private $fixtureLoader;
-//
-// /**
-// * @var EntityManagerInterface
-// */
-// private $em;
-//
-// /**
-// * @var ApiConfig
-// */
-// private $apiConfig;
-//
-// /**
-// * @var array
-// */
-// private $typeMap = [
-// 'bool' => 'boolean',
-// 'boolean' => 'boolean',
-// 'int' => 'integer',
-// 'integer' => 'integer',
-// 'float' => 'number',
-// 'number' => 'number',
-// 'numeric' => 'number',
-// 'numericint' => 'integer',
-// 'timestamp' => 'integer',
-// 'string' => 'string',
-// 'unicode' => ['string', 'unicode'],
-// 'email' => ['string', 'email'],
-// 'url' => ['string', 'url'],
-// 'uri' => ['string', 'uri'],
-// 'pattern' => null,
-// 'alnum' => ['string', 'alphanumeric'],
-// 'alpha' => ['string', 'alphabetic'],
-// 'digit' => ['string', 'numeric'],
-// 'lower' => ['string', 'lowercase'],
-// 'upper' => ['string', 'uppercase']
-// ];
-//
-// public function __construct(
-// RouteList $router,
-// IPresenterFactory $presenterFactory,
-// AliceLoader $loader,
-// EntityManagerInterface $em,
-// ApiConfig $apiConfig
-// ) {
-// parent::__construct();
-// $this->router = $router;
-// $this->presenterFactory = $presenterFactory;
-// $this->fixtureLoader = $loader;
-// $this->em = $em;
-// $this->apiConfig = $apiConfig;
-// }
-//
-// protected function configure()
-// {
-// $this->setName("swagger:generate")->setDescription("Generate a swagger specification file from existing code");
-// $this->addArgument(
-// "source",
-// InputArgument::OPTIONAL,
-// "A YAML Swagger file to use as a template for the generated file",
-// null
-// );
-// $this->addOption("save", null, InputOption::VALUE_NONE, "Save the output back to the source file");
-// }
-//
-// protected function setArrayDefault(&$array, $key, $default)
-// {
-// if (!array_key_exists($key, $array)) {
-// $array[$key] = $default;
-// return true;
-// }
-//
-// return false;
-// }
-//
-// protected function execute(InputInterface $input, OutputInterface $output)
-// {
-// $apiRoutes = $this->findAPIRouteList();
-//
-// if (!$apiRoutes) {
-// $output->writeln("No suitable routes found");
-// return 1;
-// }
-//
-// $source = $input->getArgument("source");
-// $save = $input->getOption("save");
-//
-// if ($save && $source === null) {
-// $output->writeln("--save cannot be used without a source file");
-// return 1;
-// }
-//
-// $document = $source ? Yaml::parse(file_get_contents($source)) : [];
-// $basePath = ltrim(Arrays::get($document, "basePath", "/v1"), "/");
-//
-// $this->setArrayDefault($document, "info", []);
-// $document["info"]["version"] = $this->apiConfig->getVersion();
-//
-// $this->setArrayDefault($document, "paths", []);
-// $paths = &$document["paths"];
-//
-// $this->setArrayDefault($document, "tags", []);
-// $tags = &$document["tags"];
-//
-// $defaultSecurity = null;
-// $securityDefinitions = [];
-//
-// if (array_key_exists('securityDefinitions', $document)) {
-// $securityDefinitions = array_keys($document['securityDefinitions']);
-//
-// if (count($securityDefinitions) > 0) {
-// $defaultSecurity = $securityDefinitions[0];
-// }
-// }
-//
-// foreach ($apiRoutes as $routeData) {
-// $route = $routeData["route"];
-// $parentRoute = $routeData["parent"];
-//
-// $method = self::getPropertyValue($route, "method");
-// $actualRoute = self::getPropertyValue($route, "route");
-//
-// $metadata = self::getPropertyValue($actualRoute, "metadata");
-// $mask = self::getPropertyValue($actualRoute, "mask");
-//
-// if (!Strings::startsWith($mask, $basePath)) {
-// continue;
-// }
-//
-// $mask = substr(str_replace(["<", ">"], ["{", "}"], $mask), strlen($basePath));
-//
-// $this->setArrayDefault($paths, $mask, []);
-// $this->setArrayDefault($paths[$mask], strtolower($method), []);
-//
-// // TODO hack - we need a better way of getting module names from nested RouteList objects
-// $module = "V1:" . self::getPropertyValue($parentRoute, "module");
-// $this->fillPathEntry(
-// $metadata,
-// $paths[$mask][strtolower($method)],
-// $module,
-// $defaultSecurity,
-// function ($text) use ($output, $method, $mask) {
-// $output->writeln("Endpoint $method $mask: $text");
-// }
-// );
-// $this->makePresenterTag($metadata, $module, $tags, $paths[$mask][strtolower($method)]);
-// }
-//
-// $this->setArrayDefault($document, "definitions", []);
-// $this->fillEntityExamples($document["definitions"]);
-//
-// $yaml = Yaml::dump($document, 10, 2);
-// $yaml = Strings::replace($yaml, '/(?<=parameters:)\s*\{\s*\}/', " [ ]"); // :-!
-// $yaml = Strings::replace($yaml, '/(?<=tags:)\s*\{\s*\}/', " [ ]"); // :-!
-//
-// foreach ($securityDefinitions as $definition) {
-// $yaml = Strings::replace($yaml, '/(?<=' . $definition . ':)\s*\{\s*\}/', " [ ]"); // :-!
-// }
-//
-// // $output->write($yaml);
-//
-// if ($save) {
-// file_put_contents($source, $yaml);
-// }
-//
-// return 0;
-// }
-//
-// private function fillPathEntry(
-// array $metadata,
-// array &$entry,
-// $module,
-// $defaultSecurity = null,
-// callable $warning = null
-// ) {
-// if ($warning === null) {
-// $warning = function ($text) {
-// };
-// }
-//
-// if (count($entry["tags"]) > 1) {
-// $warning("Multiple tags");
-// }
-//
-// $presenterName = $module . $metadata["presenter"]["value"];
-// $action = $metadata["action"]["value"] ?: "default";
-//
-// /** @var Presenter $presenter */
-// $presenter = $this->presenterFactory->createPresenter($presenterName);
-// $methodName = $presenter->formatActionMethod($action);
-//
-// try {
-// $method = Method::from(get_class($presenter), $methodName);
-// } catch (ReflectionException $exception) {
-// return null;
-// }
-//
-// $annotations = $method->getAnnotations();
-//
-// $entry["description"] = $method->getDescription() ?: "";
-// $this->setArrayDefault($entry, "parameters", []);
-// $this->setArrayDefault($entry, "responses", []);
-//
-// $existingParams = [];
-//
-// foreach ($entry["parameters"] as $paramEntry) {
-// $existingParams[$paramEntry["name"]] = false;
-// }
-//
-// foreach (Arrays::get($annotations, "Param", []) as $annotation) {
-// if ($annotation instanceof ArrayHash) {
-// $annotation = get_object_vars($annotation);
-// }
-//
-// $required = Arrays::get($annotation, "required", false);
-// $validation = Arrays::get($annotation, "validation", "");
-// $in = $annotation["type"] === "post" ? "formData" : "query";
-// $description = Arrays::get($annotation, "description", "");
-// $this->fillParamEntry($entry, $annotation["name"], $in, $required, $validation, $description);
-//
-// $existingParams[$annotation["name"]] = true;
-// }
-//
-// $parameterAnnotations = Arrays::get($annotations, "param", []);
-//
-// foreach ($method->getParameters() as $methodParameter) {
-// $in = $methodParameter->isOptional() ? "query" : "path";
-// $description = "";
-// $validation = "string";
-// $existingParams[$methodParameter->getName()] = true;
-//
-// foreach ($parameterAnnotations as $annotation) {
-// $annotationParts = explode(" ", $annotation, 3);
-// $firstPart = Arrays::get($annotationParts, 0, null);
-// $secondPart = Arrays::get($annotationParts, 1, null);
-//
-// if ($secondPart === "$" . $methodParameter->getName()) {
-// $validation = $firstPart;
-// } else {
-// if ($firstPart === "$" . $methodParameter->getName()) {
-// $validation = $secondPart;
-// } else {
-// continue;
-// }
-// }
-//
-// $description = Arrays::get($annotationParts, 2, "");
-// }
-//
-// $this->fillParamEntry(
-// $entry,
-// $methodParameter->getName(),
-// $in,
-// !$methodParameter->isOptional(),
-// $validation ?? "",
-// $description
-// );
-// }
-//
-// foreach ($existingParams as $param => $exists) {
-// if (!$exists) {
-// $warning("Unknown parameter $param");
-// }
-// }
-//
-// $this->setArrayDefault($entry["responses"], "200", []);
-//
-// /** @var ?IAnnotation $loggedInAnnotation */
-// $loggedInAnnotation = $method->getAnnotation("LoggedIn");
-// $isLoginNeeded = $presenter->getReflection()->getAnnotation("LoggedIn") || $loggedInAnnotation;
-//
-// if ($isLoginNeeded) {
-// $this->setArrayDefault($entry["responses"], "401", []);
-//
-// if ($defaultSecurity !== null) {
-// $this->setArrayDefault($entry, 'security', [[$defaultSecurity => []]]);
-// }
-// } elseif (array_key_exists("401", $entry["responses"])) {
-// $warning(
-// sprintf(
-// "Method %s is not annotated with @LoggedIn, but corresponding endpoint has 401 in its response list",
-// $method->name
-// )
-// );
-// }
-//
-// /** @var ?IAnnotation $userIsAllowedAnnotation */
-// $userIsAllowedAnnotation = $method->getAnnotation("UserIsAllowed");
-// /** @var ?IAnnotation $roleAnnotation */
-// $roleAnnotation = $method->getAnnotation("Role");
-// $isAuthFailurePossible = $userIsAllowedAnnotation
-// || $presenter->getReflection()->getAnnotation("Role")
-// || $roleAnnotation;
-//
-// if ($isAuthFailurePossible) {
-// $this->setArrayDefault($entry["responses"], "403", []);
-// } elseif (array_key_exists("403", $entry["responses"])) {
-// $warning(
-// sprintf(
-// "Method %s is not annotated with @UserIsAllowed, but corresponding endpoint has 403 in its response list",
-// $method->name
-// )
-// );
-// }
-//
-// return $entry;
-// }
-//
-// /**
-// * @param array $entry
-// * @param string $name
-// * @param string $in
-// * @param bool $required
-// * @param string $validation
-// * @param string $description
-// */
-// private function fillParamEntry(array &$entry, $name, $in, $required, $validation, $description)
-// {
-// $paramEntryFound = false;
-//
-// foreach ($entry["parameters"] as $i => $parameter) {
-// if ($parameter["name"] === $name) {
-// $paramEntry = &$entry["parameters"][$i];
-// $paramEntryFound = true;
-// break;
-// }
-// }
-//
-// if (!$paramEntryFound) {
-// $entry["parameters"][] = [
-// "name" => $name
-// ];
-//
-// $paramEntry = &$entry["parameters"][count($entry["parameters"]) - 1];
-// }
-//
-// $paramEntry["in"] = $in;
-// $paramEntry["required"] = $required;
-//
-// if ($in === "path") {
-// $paramEntry["required"] = true;
-// } else {
-// if ($in === "query") {
-// $this->setArrayDefault($paramEntry, "required", false);
-// }
-// }
-//
-// $paramEntry = array_merge($paramEntry, $this->translateType($validation));
-// $paramEntry["description"] = $description;
-// }
-//
-// private function findAPIRouteList()
-// {
-// $queue = [$this->router];
-//
-// while (count($queue) != 0) {
-// $cursor = array_shift($queue);
-//
-// if ($cursor instanceof RouteList) {
-// foreach ($cursor as $item) {
-// if ($item instanceof MethodRoute) {
-// yield [
-// "parent" => $cursor,
-// "route" => $item
-// ];
-// }
-//
-// if ($item instanceof RouteList) {
-// array_push($queue, $item);
-// }
-// }
-// }
-// }
-//
-// return null;
-// }
-//
-// private static function getPropertyValue($object, $propertyName)
-// {
-// $class = new ReflectionClass($object);
-//
-// do {
-// try {
-// $property = $class->getProperty($propertyName);
-// } catch (ReflectionException $exception) {
-// $class = $class->getParentClass();
-// $property = null;
-// }
-// } while ($property === null && $class !== null);
-//
-// $property->setAccessible(true);
-// return $property->getValue($object);
-// }
-//
-// private function translateType(string $type): array
-// {
-// if (!$type) {
-// return [];
-// }
-//
-// $validation = null;
-//
-// if (Strings::contains($type, ':')) {
-// list($type, $validation) = explode(':', $type);
-// }
-//
-// $translation = Arrays::get($this->typeMap, $type, null);
-// if (is_array($translation)) {
-// $typeInfo = [
-// 'type' => $translation[0],
-// 'format' => $translation[1]
-// ];
-// } else {
-// if ($translation !== null) {
-// $typeInfo = [
-// 'type' => $translation
-// ];
-// } else {
-// return [];
-// }
-// }
-//
-// if ($validation && Strings::contains($validation, '..')) {
-// list($min, $max) = explode('..', $validation);
-// if ($min) {
-// $typeInfo['minLength'] = intval($min);
-// }
-//
-// if ($max) {
-// $typeInfo['maxLength'] = intval($max);
-// }
-// } else {
-// if ($validation) {
-// $typeInfo['minLength'] = intval($validation);
-// $typeInfo['maxLength'] = intval($validation);
-// }
-// }
-//
-// return $typeInfo;
-// }
-//
-// private function fillEntityExamples(array &$target)
-// {
-// // Load fixtures from the "base" and "demo" groups
-// $fixtureDir = __DIR__ . "/../../fixtures";
-//
-// $finder = Finder::findFiles("*.neon", "*.yaml", "*.yml")
-// ->in($fixtureDir . "/base", $fixtureDir . "/demo");
-//
-// $files = [];
-//
-// /** @var SplFileInfo $file */
-// foreach ($finder as $file) {
-// $files[] = $file->getRealPath();
-// }
-//
-// sort($files);
-//
-// // Create a DB in memory so that we don't mess up the default one
-// $em = EntityManager::create(
-// new Connection(
-// ['url' => 'sqlite://:memory:'],
-// $this->em->getConnection()->getDriver(),
-// $this->em->getConfiguration(),
-// $this->em->getEventManager()
-// ),
-// $this->em->getConfiguration(),
-// $this->em->getEventManager()
-// );
-//
-// $schemaTool = new SchemaTool($em);
-// $schemaTool->createSchema($em->getMetadataFactory()->getAllMetadata());
-//
-// // Load fixtures and persist them
-// foreach ($files as $file) {
-// $loadedEntities = $this->fixtureLoader->load($file);
-//
-// foreach ($loadedEntities as $entity) {
-// $em->persist($entity);
-// }
-// }
-//
-// $em->flush();
-// $em->clear();
-//
-// $entityExamples = [];
-// foreach ($em->getMetadataFactory()->getAllMetadata() as $metadata) {
-// $name = $metadata->getName();
-// $reflection = ClassType::from($name);
-// if (Strings::startsWith($name, "App") && !$reflection->isAbstract()) {
-// $entityExamples[] = $em->getRepository($name)->findAll()[0];
-// }
-// }
-//
-// // Dump serializable entities into the document
-// foreach ($entityExamples as $entity) {
-// if ($entity instanceof JsonSerializable) {
-// $entityClass = ClassType::from($entity);
-// $entityData = Json::decode(Json::encode($entity), Json::FORCE_ARRAY);
-// $this->updateEntityEntry($target, $entityClass->getShortName(), $entityData);
-// }
-// }
-// }
-//
-// private function updateEntityEntry(array &$entry, $key, $value)
-// {
-// $type = is_array($value)
-// ? (Arrays::isList($value) ? "array" : "object")
-// : gettype($value);
-//
-// $this->setArrayDefault($entry, $key, []);
-//
-// // If a property value is a reference, just skip it
-// if (count($entry[$key]) == 1 && array_key_exists('$ref', $entry[$key])) {
-// return;
-// }
-//
-// if ($type === "object") {
-// $entry[$key]["type"] = "object";
-// $this->setArrayDefault($entry[$key], "properties", []);
-//
-// foreach ($value as $objectKey => $objectValue) {
-// $this->updateEntityEntry($entry[$key]["properties"], $objectKey, $objectValue);
-// }
-// } else {
-// if ($type === "array") {
-// $entry[$key]["type"] = "array";
-// $this->setArrayDefault($entry[$key], "items", []);
-//
-// if (count($value) > 0) {
-// $this->updateEntityEntry($entry[$key], "items", $value[0]);
-// }
-// } else {
-// $this->setArrayDefault($entry[$key], "type", $type);
-// if ($entry[$key]["type"] === $type && $value !== null) {
-// $entry[$key]["example"] = $value;
-// }
-// }
-// }
-// }
-//
-// private function makePresenterTag($metadata, $module, array &$tags, array &$entry)
-// {
-// $presenterName = $metadata["presenter"]["value"];
-// $fullPresenterName = $module . $presenterName;
-//
-// /** @var Presenter $presenter */
-// $presenter = $this->presenterFactory->createPresenter($fullPresenterName);
-//
-// $tag = strtolower(Strings::replace($presenterName, '/(?!^)([A-Z])/', '-\1'));
-// $tagEntry = [];
-// $tagEntryFound = false;
-//
-// foreach ($tags as $i => $tagEntry) {
-// if ($tagEntry["name"] === $tag) {
-// $tagEntryFound = true;
-// $tagEntry = &$tags[$i];
-// break;
-// }
-// }
-//
-// if (!$tagEntryFound) {
-// $tags[] = [
-// "name" => $tag
-// ];
-//
-// $tagEntry = &$tags[count($tags) - 1];
-// }
-//
-// $tagEntry["description"] = (new ClassType($presenter))->getDescription() ?: "";
-//
-// $this->setArrayDefault($entry, "tags", []);
-// $entry["tags"][] = $tag;
-// $entry["tags"] = array_unique($entry["tags"]);
-// }
-// }
+/**
+ * Command that consumes a temporary file containing endpoint annotations and generates a swagger documentation.
+ */
+class GenerateSwagger extends Command
+{
+ protected static $defaultName = 'swagger:generate';
+
+ protected function configure()
+ {
+ $this->setName(self::$defaultName)->setDescription(
+ 'Generate an OpenAPI documentation from the temporary file created by the swagger:annotate command.'
+ . ' The temporary file is deleted afterwards.'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $path = __DIR__ . '/../V1Module/presenters/_autogenerated_annotations_temp.php';
+
+ // check if file exists
+ if (!file_exists($path)) {
+ $output->writeln("Error in GenerateSwagger: Temp annotation file not found.");
+ return Command::FAILURE;
+ }
+
+ $openapi = Generator::scan([$path]);
+
+ $output->writeln($openapi->toYaml());
+
+ // delete the temp file
+ unlink($path);
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/app/commands/SwaggerAnnotator.php b/app/commands/SwaggerAnnotator.php
new file mode 100644
index 000000000..1c9c9321b
--- /dev/null
+++ b/app/commands/SwaggerAnnotator.php
@@ -0,0 +1,159 @@
+setName(self::$defaultName)->setDescription(
+ "Extracts endpoint method annotations and puts them into a temporary file that can be used to generate"
+ . " an OpenAPI documentation. The file is located at {$filePath}"
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ // create a temporary file containing transpiled annotations usable by the external library (Swagger-PHP)
+ $fileBuilder = new TempAnnotationFileBuilder(self::$autogeneratedAnnotationFilePath);
+ $fileBuilder->startClass('__Autogenerated_Annotation_Controller__', '1.0', 'ReCodEx API');
+
+ // get all routes of the api
+ $routes = $this->getRoutes();
+ foreach ($routes as $routeObj) {
+ // extract class and method names of the endpoint
+ $metadata = $this->extractMetadata($routeObj);
+ $route = $this->extractRoute($routeObj);
+ $className = self::$presenterNamespace . $metadata['class'];
+
+ // extract data from the existing annotations
+ $annotationData = AnnotationHelper::extractAnnotationData($className, $metadata['method'], $route);
+
+ // add an empty method to the file with the transpiled annotations
+ $fileBuilder->addAnnotatedMethod($metadata['method'], $annotationData->toSwaggerAnnotations($route));
+ }
+ $fileBuilder->endClass();
+
+ return Command::SUCCESS;
+ } catch (Exception $e) {
+ $output->writeln("Error in SwaggerAnnotator: {$e->getMessage()}");
+
+ return Command::FAILURE;
+ }
+ }
+
+ /**
+ * Finds all route objects of the API
+ * @return array Returns an array of all found route objects.
+ */
+ private function getRoutes(): array
+ {
+ $router = \App\V1Module\RouterFactory::createRouter();
+
+ // find all route object using a queue
+ $queue = [$router];
+ $routes = [];
+ while (count($queue) != 0) {
+ $cursor = array_shift($queue);
+
+ if ($cursor instanceof RouteList) {
+ foreach ($cursor->getRouters() as $item) {
+ // lists contain routes or nested lists
+ if ($item instanceof RouteList) {
+ array_push($queue, $item);
+ } else {
+ // the first route is special and holds no useful information for annotation
+ if (get_parent_class($item) !== MethodRoute::class) {
+ continue;
+ }
+
+ $routes[] = $this->getPropertyValue($item, "route");
+ }
+ }
+ }
+ }
+
+ return $routes;
+ }
+
+ /**
+ * Extracts the route string from a route object. Replaces '<..>' in the route with '{...}'.
+ * @param mixed $routeObj
+ */
+ private function extractRoute($routeObj): string
+ {
+ $mask = self::getPropertyValue($routeObj, "mask");
+
+ // sample: replaces '/users/' with '/users/{id}'
+ $mask = str_replace(["<", ">"], ["{", "}"], $mask);
+ return "/" . $mask;
+ }
+
+ /**
+ * Extracts the class and method names of the endpoint handler.
+ * @param mixed $routeObj The route object representing the endpoint.
+ * @return string[] Returns a dictionary [ "class" => ..., "method" => ...]
+ */
+ private function extractMetadata($routeObj)
+ {
+ $metadata = self::getPropertyValue($routeObj, "metadata");
+ $presenter = $metadata["presenter"]["value"];
+ $action = $metadata["action"]["value"];
+
+ // if the name is empty, the method will be called 'actionDefault'
+ if ($action === null) {
+ $action = "default";
+ }
+
+ return [
+ "class" => $presenter . "Presenter",
+ "method" => "action" . ucfirst($action),
+ ];
+ }
+
+ /**
+ * Helper function that can extract a property value from an arbitrary object where
+ * the property can be private.
+ * @param mixed $object The object to extract from.
+ * @param string $propertyName The name of the property.
+ * @return mixed Returns the value of the property.
+ */
+ private static function getPropertyValue($object, string $propertyName): mixed
+ {
+ $class = new ReflectionClass($object);
+
+ do {
+ try {
+ $property = $class->getProperty($propertyName);
+ } catch (ReflectionException $exception) {
+ $class = $class->getParentClass();
+ $property = null;
+ }
+ } while ($property === null && $class !== null);
+
+ $property->setAccessible(true);
+ return $property->getValue($object);
+ }
+}
diff --git a/app/config/config.neon b/app/config/config.neon
index fe0ef69b1..3f9e704eb 100644
--- a/app/config/config.neon
+++ b/app/config/config.neon
@@ -318,6 +318,8 @@ services:
- App\Console\AsyncJobsUpkeep(%async.upkeep%)
- App\Console\GeneralStatsNotification
- App\Console\ExportDatabase
+ - App\Console\GenerateSwagger
+ - App\Console\SwaggerAnnotator
- App\Console\CleanupLocalizedTexts
- App\Console\CleanupExerciseConfigs
- App\Console\CleanupPipelineConfigs
diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php
new file mode 100644
index 000000000..977a5a1fa
--- /dev/null
+++ b/app/helpers/Swagger/AnnotationData.php
@@ -0,0 +1,91 @@
+httpMethod = $httpMethod;
+ $this->pathParams = $pathParams;
+ $this->queryParams = $queryParams;
+ $this->bodyParams = $bodyParams;
+ }
+
+ /**
+ * Creates a method annotation string parsable by the swagger generator.
+ * Example: if the method name is 'Put', the method will return '@OA\\PUT'.
+ * @return string Returns the method annotation.
+ */
+ private function getHttpMethodAnnotation(): string
+ {
+ // sample: converts 'PUT' to 'Put'
+ $httpMethodString = ucfirst(strtolower($this->httpMethod->name));
+ return "@OA\\" . $httpMethodString;
+ }
+
+ /**
+ * Creates a JSON request body annotation string parsable by the swagger generator.
+ * Example: if the request body contains only the 'url' property, this method will produce:
+ * '@OA\RequestBody(@OA\MediaType(mediaType="application/json",@OA\Schema(@OA\Property(property="url",type="string"))))'
+ * @return string|null Returns the annotation string or null, if there are no body parameters.
+ */
+ private function getBodyAnnotation(): string | null
+ {
+ if (count($this->bodyParams) === 0) {
+ return null;
+ }
+
+ ///TODO: The swagger generator only supports JSON due to the hardcoded mediaType below
+ $head = '@OA\RequestBody(@OA\MediaType(mediaType="application/json",@OA\Schema';
+ $body = new ParenthesesBuilder();
+
+ foreach ($this->bodyParams as $bodyParam) {
+ $body->addValue($bodyParam->toPropertyAnnotation());
+ }
+
+ return $head . $body->toString() . "))";
+ }
+
+ /**
+ * Converts the extracted annotation data to a string parsable by the Swagger-PHP library.
+ * @param string $route The route of the handler this set of data represents.
+ * @return string Returns the transpiled annotations on a single line.
+ */
+ public function toSwaggerAnnotations(string $route)
+ {
+ $httpMethodAnnotation = $this->getHttpMethodAnnotation();
+ $body = new ParenthesesBuilder();
+ $body->addKeyValue("path", $route);
+
+ foreach ($this->pathParams as $pathParam) {
+ $body->addValue($pathParam->toParameterAnnotation());
+ }
+ foreach ($this->queryParams as $queryParam) {
+ $body->addValue($queryParam->toParameterAnnotation());
+ }
+
+ $jsonProperties = $this->getBodyAnnotation();
+ if ($jsonProperties !== null) {
+ $body->addValue($jsonProperties);
+ }
+
+ ///TODO: A placeholder for the response type. This has to be replaced with the autogenerated meta-view
+ /// response data structure in the future.
+ $body->addValue('@OA\Response(response="200",description="The data")');
+ return $httpMethodAnnotation . $body->toString();
+ }
+}
diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php
new file mode 100644
index 000000000..1b2ba8840
--- /dev/null
+++ b/app/helpers/Swagger/AnnotationHelper.php
@@ -0,0 +1,249 @@
+getMethod($methodName);
+ }
+
+ /**
+ * Searches an array of annotations for any line starting with a valid HTTP method.
+ * @param array $annotations An array of annotations.
+ * @return \App\Helpers\Swagger\HttpMethods|null Returns the HTTP method or null if none present.
+ */
+ private static function extractAnnotationHttpMethod(array $annotations): HttpMethods | null
+ {
+ // get string names of the enumeration
+ $cases = HttpMethods::cases();
+ $methods = [];
+ foreach ($cases as $case) {
+ $methods["@{$case->name}"] = $case;
+ }
+
+ // check if the annotations have an http method
+ foreach ($methods as $methodString => $methodEnum) {
+ if (in_array($methodString, $annotations)) {
+ return $methodEnum;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts standart doc comments from endpoints, such as '@param string $id An identifier'.
+ * Based on the HTTP route of the endpoint, the extracted param can be identified as either a path or
+ * query parameter.
+ * @param array $annotations An array of annotations.
+ * @param string $route The HTTP route of the endpoint.
+ * @return array Returns an array of AnnotationParameterData objects describing the parameters.
+ */
+ private static function extractStandardAnnotationParams(array $annotations, string $route): array
+ {
+ $routeParams = self::getRoutePathParamNames($route);
+
+ $params = [];
+ foreach ($annotations as $annotation) {
+ // assumed that all query parameters have a @param annotation
+ if (str_starts_with($annotation, "@param")) {
+ // sample: @param string $id Identifier of the user
+ $tokens = explode(" ", $annotation);
+ $type = $tokens[1];
+ // assumed that all names start with $
+ $name = substr($tokens[2], 1);
+ $description = implode(" ", array_slice($tokens, 3));
+
+ // figure out where the parameter is located
+ $location = 'query';
+ if (in_array($name, $routeParams)) {
+ $location = 'path';
+ }
+
+ $descriptor = new AnnotationParameterData($type, $name, $description, $location);
+ $params[] = $descriptor;
+ }
+ }
+ return $params;
+ }
+
+ /**
+ * Converts an array of assignment string to an associative array.
+ * @param array $expressions An array containing values in the following format: 'key="value"'.
+ * @return array Returns an associative array made from the string array.
+ */
+ private static function stringArrayToAssociativeArray(array $expressions): array
+ {
+ $dict = [];
+ //sample: [ 'name="uiData"', 'validation="array|null"' ]
+ foreach ($expressions as $expression) {
+ $tokens = explode('="', $expression);
+ $name = $tokens[0];
+ // remove the '"' at the end
+ $value = substr($tokens[1], 0, -1);
+ $dict[$name] = $value;
+ }
+ return $dict;
+ }
+
+ /**
+ * Extracts annotation parameter data from Nette annotations starting with the '@Param' prefix.
+ * @param array $annotations An array of annotations.
+ * @return array Returns an array of AnnotationParameterData objects describing the parameters.
+ */
+ private static function extractNetteAnnotationParams(array $annotations): array
+ {
+ $bodyParams = [];
+ $prefix = "@Param";
+ foreach ($annotations as $annotation) {
+ // assumed that all body parameters have a @Param annotation
+ if (str_starts_with($annotation, $prefix)) {
+ // sample: @Param(type="post", name="uiData", validation="array|null",
+ // description="Structured user-specific UI data")
+ // remove '@Param(' from the start and ')' from the end
+ $body = substr($annotation, strlen($prefix) + 1, -1);
+ $tokens = explode(", ", $body);
+ $values = self::stringArrayToAssociativeArray($tokens);
+ $descriptor = new AnnotationParameterData(
+ $values["validation"],
+ $values["name"],
+ $values["description"],
+ $values["type"]
+ );
+ $bodyParams[] = $descriptor;
+ }
+ }
+ return $bodyParams;
+ }
+
+ /**
+ * Returns all method annotation lines as an array.
+ * Lines not starting with '@' are assumed to be continuations of a parent line starting with @ (or the initial
+ * line not starting with '@') and are merged into a single line.
+ * @param string $className The name of the containing class.
+ * @param string $methodName The name of the method.
+ * @return array Returns an array of the annotation lines.
+ */
+ private static function getMethodAnnotations(string $className, string $methodName): array
+ {
+ $annotations = self::getMethod($className, $methodName)->getDocComment();
+ $lines = preg_split("/\r\n|\n|\r/", $annotations);
+
+ // trims whitespace and asterisks
+ // assumes that asterisks are not used in some meaningful way at the beginning and end of a line
+ foreach ($lines as &$line) {
+ $line = trim($line);
+ $line = trim($line, "*");
+ $line = trim($line);
+ }
+
+ // removes the first and last line
+ // assumes that the first line is '/**' and the last line '*/' (or '/' after trimming)
+ $lines = array_slice($lines, 1, -1);
+
+ $merged = [];
+ for ($i = 0; $i < count($lines); $i++) {
+ $line = $lines[$i];
+
+ // skip lines not starting with '@'
+ if ($line[0] !== "@") {
+ continue;
+ }
+
+ // merge lines not starting with '@' with their parent lines starting with '@'
+ while ($i + 1 < count($lines) && $lines[$i + 1][0] !== "@") {
+ $line .= " " . $lines[$i + 1];
+ $i++;
+ }
+
+ $merged[] = $line;
+ }
+
+ return $merged;
+ }
+
+ /**
+ * Extracts strings enclosed by curly brackets.
+ * @param string $route The source string.
+ * @return array Returns the tokens extracted from the brackets.
+ */
+ private static function getRoutePathParamNames(string $route): array
+ {
+ // sample: from '/users/{id}/{name}' generates ['id', 'name']
+ preg_match_all('/\{([A-Za-z0-9 ]+?)\}/', $route, $out);
+ return $out[1];
+ }
+
+ /**
+ * Extracts the annotation data of an endpoint. The data contains request parameters based on their type
+ * and the HTTP method.
+ * @param string $className The name of the containing class.
+ * @param string $methodName The name of the endpoint method.
+ * @param string $route The route to the method.
+ * @throws Exception Thrown when the parser encounters an unknown parameter location (known locations are
+ * path, query and post)
+ * @return \App\Helpers\Swagger\AnnotationData Returns a data object containing the parameters and HTTP method.
+ */
+ public static function extractAnnotationData(string $className, string $methodName, string $route): AnnotationData
+ {
+ $methodAnnotations = self::getMethodAnnotations($className, $methodName);
+
+ $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations);
+ $standardAnnotationParams = self::extractStandardAnnotationParams($methodAnnotations, $route);
+ $netteAnnotationParams = self::extractNetteAnnotationParams($methodAnnotations);
+ $params = array_merge($standardAnnotationParams, $netteAnnotationParams);
+
+ $pathParams = [];
+ $queryParams = [];
+ $bodyParams = [];
+
+ foreach ($params as $param) {
+ if ($param->location === 'path') {
+ $pathParams[] = $param;
+ } elseif ($param->location === 'query') {
+ $queryParams[] = $param;
+ } elseif ($param->location === 'post') {
+ $bodyParams[] = $param;
+ } else {
+ throw new Exception("Error in extractAnnotationData: Unknown param location: {$param->location}");
+ }
+ }
+
+
+ $data = new AnnotationData($httpMethod, $pathParams, $queryParams, $bodyParams);
+ return $data;
+ }
+
+ /**
+ * Filters annotation lines starting with a prefix.
+ * @param array $annotations An array of annotations.
+ * @param string $type The prefix with which the lines should start, such as '@param'.
+ * @return array Returns an array of filtered annotations.
+ */
+ public static function filterAnnotations(array $annotations, string $type)
+ {
+ $rows = [];
+ foreach ($annotations as $annotation) {
+ if (str_starts_with($annotation, $type)) {
+ $rows[] = $annotation;
+ }
+ }
+ return $rows;
+ }
+}
diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php
new file mode 100644
index 000000000..d915f32d3
--- /dev/null
+++ b/app/helpers/Swagger/AnnotationParameterData.php
@@ -0,0 +1,141 @@
+ 'boolean',
+ 'boolean' => 'boolean',
+ 'array' => 'array',
+ 'int' => 'integer',
+ 'integer' => 'integer',
+ 'float' => 'number',
+ 'number' => 'number',
+ 'numeric' => 'number',
+ 'numericint' => 'integer',
+ 'timestamp' => 'integer',
+ 'string' => 'string',
+ 'unicode' => 'string',
+ 'email' => 'string',
+ 'url' => 'string',
+ 'uri' => 'string',
+ 'pattern' => null,
+ 'alnum' => 'string',
+ 'alpha' => 'string',
+ 'digit' => 'string',
+ 'lower' => 'string',
+ 'upper' => 'string',
+ ];
+
+ public function __construct(
+ string | null $dataType,
+ string $name,
+ string | null $description,
+ string $location
+ ) {
+ $this->dataType = $dataType;
+ $this->name = $name;
+ $this->description = $description;
+ $this->location = $location;
+ }
+
+ private function isDatatypeNullable(): bool
+ {
+ // if the dataType is not specified (it is null), it means that the annotation is not
+ // complete and defaults to a non nullable string
+ if ($this->dataType === null) {
+ return false;
+ }
+
+ // assumes that the typename ends with '|null'
+ if (str_ends_with($this->dataType, self::$nullableSuffix)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the swagger type associated with the annotation data type.
+ * @return string Returns the name of the swagger type.
+ */
+ private function getSwaggerType(): string
+ {
+ // if the type is not specified, default to a string
+ $type = 'string';
+ $typename = $this->dataType;
+ if ($typename !== null) {
+ if ($this->isDatatypeNullable()) {
+ $typename = substr($typename, 0, -strlen(self::$nullableSuffix));
+ }
+
+ if (self::$typeMap[$typename] === null) {
+ ///TODO: Return the commented exception below once the meta-view formats are implemented.
+ /// This detaults to strings because custom types like 'email' are not supported yet.
+ return 'string';
+ }
+ //throw new \InvalidArgumentException("Error in getSwaggerType: Unknown typename: {$typename}");
+
+ $type = self::$typeMap[$typename];
+ }
+ return $type;
+ }
+
+ /**
+ * Generates swagger schema annotations based on the data type.
+ * @return string Returns the annotation.
+ */
+ private function generateSchemaAnnotation(): string
+ {
+ $head = "@OA\\Schema";
+ $body = new ParenthesesBuilder();
+
+ $body->addKeyValue("type", $this->getSwaggerType());
+ return $head . $body->toString();
+ }
+
+ /**
+ * Converts the object to a @OA\Parameter(...) annotation string
+ */
+ public function toParameterAnnotation(): string
+ {
+ $head = "@OA\\Parameter";
+ $body = new ParenthesesBuilder();
+
+ $body->addKeyValue("name", $this->name);
+ $body->addKeyValue("in", $this->location);
+ $body->addKeyValue("required", !$this->isDatatypeNullable());
+ if ($this->description !== null) {
+ $body->addKeyValue("description", $this->description);
+ }
+
+ $body->addValue($this->generateSchemaAnnotation());
+
+ return $head . $body->toString();
+ }
+
+ /**
+ * Generates swagger property annotations based on the data type.
+ * @return string Returns the annotation.
+ */
+ public function toPropertyAnnotation(): string
+ {
+ $head = "@OA\\Property";
+ $body = new ParenthesesBuilder();
+
+ ///TODO: Once the meta-view formats are implemented, add support for property nullability here.
+ $body->addKeyValue("property", $this->name);
+ $body->addKeyValue("type", $this->getSwaggerType());
+ return $head . $body->toString();
+ }
+}
diff --git a/app/helpers/Swagger/HttpMethods.php b/app/helpers/Swagger/HttpMethods.php
new file mode 100644
index 000000000..8bb508daf
--- /dev/null
+++ b/app/helpers/Swagger/HttpMethods.php
@@ -0,0 +1,14 @@
+tokens = [];
+ }
+
+ /**
+ * Add a token inside the parentheses in the format of: key="value"
+ * @param string $key A string key.
+ * @param mixed $value A value that will be stringified.
+ * @return \App\Helpers\Swagger\ParenthesesBuilder Returns the builder object.
+ */
+ public function addKeyValue(string $key, mixed $value): ParenthesesBuilder
+ {
+ $valueString = strval($value);
+ // strings need to be wrapped in quotes
+ if (is_string($value)) {
+ $valueString = "\"{$value}\"";
+ // convert bools to strings
+ } elseif (is_bool($value)) {
+ $valueString = ($value ? "true" : "false");
+ }
+
+ $assignment = "{$key}={$valueString}";
+ return $this->addValue($assignment);
+ }
+
+ /**
+ * Add a string token inside the parentheses.
+ * @param string $value The token to be added.
+ * @return \App\Helpers\Swagger\ParenthesesBuilder Returns the builder object.
+ */
+ public function addValue(string $value): ParenthesesBuilder
+ {
+ $this->tokens[] = $value;
+ return $this;
+ }
+
+ public function toString(): string
+ {
+ return '(' . implode(',', $this->tokens) . ')';
+ }
+}
diff --git a/app/helpers/Swagger/TempAnnotationFileBuilder.php b/app/helpers/Swagger/TempAnnotationFileBuilder.php
new file mode 100644
index 000000000..bd72ccb3e
--- /dev/null
+++ b/app/helpers/Swagger/TempAnnotationFileBuilder.php
@@ -0,0 +1,100 @@
+content = "";
+ $this->methodEntries = 0;
+ $this->filename = $filename;
+ $this->initFile();
+ }
+
+ /**
+ * Initializes the file, adding the namespace and import statements.
+ */
+ private function initFile()
+ {
+ $this->content .= "content .= "/// THIS FILE WAS AUTOGENERATED\n";
+ $this->content .= "namespace App\V1Module\Presenters;\n";
+ $this->content .= "use OpenApi\Annotations as OA;\n";
+ }
+
+ /**
+ * Creates an annotation describing the swagger version and title used.
+ * @param string $version The version of swagger.
+ * @param string $title The title of the document.
+ * @return string Returns the annotation.
+ */
+ private function createInfoAnnotation(string $version, string $title)
+ {
+ $head = "@OA\\Info";
+ $body = new ParenthesesBuilder();
+ $body->addKeyValue("version", $version);
+ $body->addKeyValue("title", $title);
+ return $head . $body->toString();
+ }
+
+ private function writeAnnotationLineWithComments(string $annotationLine)
+ {
+ $this->content .= "/**\n";
+ $this->content .= "* {$annotationLine}\n";
+ $this->content .= "*/\n";
+ }
+
+ /**
+ * Creates a class that contains all the endpoint annotation methods.
+ * Should only be called once as the first method.
+ * @param string $className The name of the class.
+ * @param string $version The version of swagger.
+ * @param string $title The name of the swagger document.
+ */
+ public function startClass(string $className, string $version, string $title)
+ {
+ $this->writeAnnotationLineWithComments($this->createInfoAnnotation($version, $title));
+ $this->content .= "class {$className} {\n";
+ }
+
+ /**
+ * Ends the class and writes the contents to the disk.
+ */
+ public function endClass()
+ {
+ $this->content .= "}\n";
+ $this->close();
+ }
+
+ /**
+ * Adds an annotated method to the class.
+ * @param string $methodName The name of the method. This does not have to be unique.
+ * @param string $annotationLine The annotation line of the method.
+ */
+ public function addAnnotatedMethod(string $methodName, string $annotationLine)
+ {
+ $this->writeAnnotationLineWithComments($annotationLine);
+ $this->content .= "public function {$methodName}{$this->methodEntries}() {}\n";
+ $this->methodEntries++;
+ }
+
+ /**
+ * Creates a file and adds the swagger content.
+ */
+ private function close()
+ {
+ $file = fopen($this->filename, "w");
+ fwrite($file, $this->content);
+ fflush($file);
+ fclose($file);
+ }
+}
diff --git a/composer.json b/composer.json
index 87f962956..a326aed96 100644
--- a/composer.json
+++ b/composer.json
@@ -62,7 +62,8 @@
"nelmio/alice": "^3.8",
"ramsey/uuid-doctrine": "^2.0",
"eluceo/ical": "^2.7",
- "league/commonmark": "^2.3"
+ "league/commonmark": "^2.3",
+ "zircote/swagger-php": "^4.10"
},
"require-dev": {
"mockery/mockery": "@stable",
diff --git a/composer.lock b/composer.lock
index 5f9788984..70da9f231 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e291a6441a13f5ee3087b3c9fb9766f5",
+ "content-hash": "cf7677a8572cdc148b2617c231b9228f",
"packages": [
{
"name": "behat/transliterator",
@@ -426,16 +426,16 @@
},
{
"name": "doctrine/annotations",
- "version": "1.14.3",
+ "version": "1.14.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
- "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af"
+ "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/annotations/zipball/fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af",
- "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af",
+ "url": "https://api.github.com/repos/doctrine/annotations/zipball/253dca476f70808a5aeed3a47cc2cc88c5cab915",
+ "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915",
"shasum": ""
},
"require": {
@@ -446,11 +446,11 @@
},
"require-dev": {
"doctrine/cache": "^1.11 || ^2.0",
- "doctrine/coding-standard": "^9 || ^10",
- "phpstan/phpstan": "~1.4.10 || ^1.8.0",
+ "doctrine/coding-standard": "^9 || ^12",
+ "phpstan/phpstan": "~1.4.10 || ^1.10.28",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
- "symfony/cache": "^4.4 || ^5.4 || ^6",
- "vimeo/psalm": "^4.10"
+ "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7",
+ "vimeo/psalm": "^4.30 || ^5.14"
},
"suggest": {
"php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations"
@@ -496,9 +496,9 @@
],
"support": {
"issues": "https://github.com/doctrine/annotations/issues",
- "source": "https://github.com/doctrine/annotations/tree/1.14.3"
+ "source": "https://github.com/doctrine/annotations/tree/1.14.4"
},
- "time": "2023-02-01T09:20:38+00:00"
+ "time": "2024-09-05T10:15:52+00:00"
},
{
"name": "doctrine/cache",
@@ -3311,16 +3311,16 @@
},
{
"name": "nette/application",
- "version": "v3.2.5",
+ "version": "v3.2.6",
"source": {
"type": "git",
"url": "https://github.com/nette/application.git",
- "reference": "1e868966c3de55a087e5ec938189ec34a1648b04"
+ "reference": "9c288cc45df467dc012504f4ad64791279720af8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/application/zipball/1e868966c3de55a087e5ec938189ec34a1648b04",
- "reference": "1e868966c3de55a087e5ec938189ec34a1648b04",
+ "url": "https://api.github.com/repos/nette/application/zipball/9c288cc45df467dc012504f4ad64791279720af8",
+ "reference": "9c288cc45df467dc012504f4ad64791279720af8",
"shasum": ""
},
"require": {
@@ -3328,10 +3328,10 @@
"nette/http": "^3.3",
"nette/routing": "^3.1",
"nette/utils": "^4.0",
- "php": "8.1 - 8.3"
+ "php": "8.1 - 8.4"
},
"conflict": {
- "latte/latte": "<2.7.1 || >=3.0.0 <3.0.12 || >=3.1",
+ "latte/latte": "<2.7.1 || >=3.0.0 <3.0.18 || >=3.1",
"nette/caching": "<3.2",
"nette/di": "<3.2",
"nette/forms": "<3.2",
@@ -3340,7 +3340,7 @@
},
"require-dev": {
"jetbrains/phpstorm-attributes": "dev-master",
- "latte/latte": "^2.10.2 || ^3.0.12",
+ "latte/latte": "^2.10.2 || ^3.0.18",
"mockery/mockery": "^2.0",
"nette/di": "^3.2",
"nette/forms": "^3.2",
@@ -3397,9 +3397,9 @@
],
"support": {
"issues": "https://github.com/nette/application/issues",
- "source": "https://github.com/nette/application/tree/v3.2.5"
+ "source": "https://github.com/nette/application/tree/v3.2.6"
},
- "time": "2024-05-13T09:10:31+00:00"
+ "time": "2024-09-10T10:08:04+00:00"
},
{
"name": "nette/bootstrap",
@@ -4118,21 +4118,21 @@
},
{
"name": "nette/php-generator",
- "version": "v4.1.5",
+ "version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/nette/php-generator.git",
- "reference": "690b00d81d42d5633e4457c43ef9754573b6f9d6"
+ "reference": "c90961e782ae86e517fe5ed732eb2b512945565b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/php-generator/zipball/690b00d81d42d5633e4457c43ef9754573b6f9d6",
- "reference": "690b00d81d42d5633e4457c43ef9754573b6f9d6",
+ "url": "https://api.github.com/repos/nette/php-generator/zipball/c90961e782ae86e517fe5ed732eb2b512945565b",
+ "reference": "c90961e782ae86e517fe5ed732eb2b512945565b",
"shasum": ""
},
"require": {
"nette/utils": "^3.2.9 || ^4.0",
- "php": "8.0 - 8.3"
+ "php": "8.0 - 8.4"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "dev-master",
@@ -4181,9 +4181,9 @@
],
"support": {
"issues": "https://github.com/nette/php-generator/issues",
- "source": "https://github.com/nette/php-generator/tree/v4.1.5"
+ "source": "https://github.com/nette/php-generator/tree/v4.1.6"
},
- "time": "2024-05-12T17:31:02+00:00"
+ "time": "2024-09-10T09:31:55+00:00"
},
{
"name": "nette/robot-loader",
@@ -5429,16 +5429,16 @@
},
{
"name": "psr/log",
- "version": "3.0.1",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "79dff0b268932c640297f5208d6298f71855c03e"
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
- "reference": "79dff0b268932c640297f5208d6298f71855c03e",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
@@ -5473,9 +5473,9 @@
"psr-3"
],
"support": {
- "source": "https://github.com/php-fig/log/tree/3.0.1"
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
},
- "time": "2024-08-21T13:31:24+00:00"
+ "time": "2024-09-11T13:17:53+00:00"
},
{
"name": "psr/simple-cache",
@@ -5839,16 +5839,16 @@
},
{
"name": "sebastian/comparator",
- "version": "6.0.2",
+ "version": "6.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81"
+ "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81",
- "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
+ "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"shasum": ""
},
"require": {
@@ -5859,12 +5859,12 @@
"sebastian/exporter": "^6.0"
},
"require-dev": {
- "phpunit/phpunit": "^11.0"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "6.0-dev"
+ "dev-main": "6.1-dev"
}
},
"autoload": {
@@ -5904,7 +5904,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.2"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.1.0"
},
"funding": [
{
@@ -5912,7 +5912,7 @@
"type": "github"
}
],
- "time": "2024-08-12T06:07:25+00:00"
+ "time": "2024-09-11T15:42:56+00:00"
},
{
"name": "sebastian/diff",
@@ -6188,16 +6188,16 @@
},
{
"name": "symfony/cache",
- "version": "v7.1.4",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
- "reference": "b61e464d7687bb7e8f677d5031c632bf3820df18"
+ "reference": "86e5296b10e4dec8c8441056ca606aedb8a3be0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache/zipball/b61e464d7687bb7e8f677d5031c632bf3820df18",
- "reference": "b61e464d7687bb7e8f677d5031c632bf3820df18",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/86e5296b10e4dec8c8441056ca606aedb8a3be0a",
+ "reference": "86e5296b10e4dec8c8441056ca606aedb8a3be0a",
"shasum": ""
},
"require": {
@@ -6265,7 +6265,7 @@
"psr6"
],
"support": {
- "source": "https://github.com/symfony/cache/tree/v7.1.4"
+ "source": "https://github.com/symfony/cache/tree/v7.1.5"
},
"funding": [
{
@@ -6281,7 +6281,7 @@
"type": "tidelift"
}
],
- "time": "2024-08-12T09:59:40+00:00"
+ "time": "2024-09-17T09:16:35+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -6361,16 +6361,16 @@
},
{
"name": "symfony/console",
- "version": "v6.4.11",
+ "version": "v6.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "42686880adaacdad1835ee8fc2a9ec5b7bd63998"
+ "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/42686880adaacdad1835ee8fc2a9ec5b7bd63998",
- "reference": "42686880adaacdad1835ee8fc2a9ec5b7bd63998",
+ "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765",
+ "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765",
"shasum": ""
},
"require": {
@@ -6435,7 +6435,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v6.4.11"
+ "source": "https://github.com/symfony/console/tree/v6.4.12"
},
"funding": [
{
@@ -6451,7 +6451,7 @@
"type": "tidelift"
}
],
- "time": "2024-08-15T22:48:29+00:00"
+ "time": "2024-09-20T08:15:52+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -6520,22 +6520,86 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
+ {
+ "name": "symfony/finder",
+ "version": "v7.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "d95bbf319f7d052082fb7af147e0f835a695e823"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823",
+ "reference": "d95bbf319f7d052082fb7af147e0f835a695e823",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v7.1.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-08-13T14:28:19+00:00"
+ },
{
"name": "symfony/polyfill-ctype",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "0424dff1c58f028c451efff2045f5d92410bd540"
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
- "reference": "0424dff1c58f028c451efff2045f5d92410bd540",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@@ -6581,7 +6645,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
@@ -6597,24 +6661,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
- "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
@@ -6659,7 +6723,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
},
"funding": [
{
@@ -6675,24 +6739,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
- "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
@@ -6740,7 +6804,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
},
"funding": [
{
@@ -6756,24 +6820,24 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
- "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@@ -6820,7 +6884,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@@ -6836,40 +6900,32 @@
"type": "tidelift"
}
],
- "time": "2024-06-19T12:30:46+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php72",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "10112722600777e02d2745716b70c5db4ca70442"
+ "reference": "fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442",
- "reference": "10112722600777e02d2745716b70c5db4ca70442",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce",
+ "reference": "fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
- "type": "library",
+ "type": "metapackage",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
- "autoload": {
- "files": [
- "bootstrap.php"
- ],
- "psr-4": {
- "Symfony\\Polyfill\\Php72\\": ""
- }
- },
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
@@ -6893,7 +6949,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-php72/tree/v1.31.0"
},
"funding": [
{
@@ -6909,24 +6965,24 @@
"type": "tidelift"
}
],
- "time": "2024-06-19T12:30:46+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.30.0",
+ "version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
- "reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
+ "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
@@ -6973,7 +7029,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@@ -6989,7 +7045,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T15:07:36+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
@@ -7359,16 +7415,16 @@
},
{
"name": "symfony/string",
- "version": "v7.1.4",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b"
+ "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b",
- "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b",
+ "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306",
+ "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306",
"shasum": ""
},
"require": {
@@ -7426,7 +7482,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.1.4"
+ "source": "https://github.com/symfony/string/tree/v7.1.5"
},
"funding": [
{
@@ -7442,20 +7498,20 @@
"type": "tidelift"
}
],
- "time": "2024-08-12T09:59:40+00:00"
+ "time": "2024-09-20T08:28:38+00:00"
},
{
"name": "symfony/type-info",
- "version": "v7.1.1",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/type-info.git",
- "reference": "60b28eb733f1453287f1263ed305b96091e0d1dc"
+ "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/type-info/zipball/60b28eb733f1453287f1263ed305b96091e0d1dc",
- "reference": "60b28eb733f1453287f1263ed305b96091e0d1dc",
+ "url": "https://api.github.com/repos/symfony/type-info/zipball/9f6094aa900d2c06bd61576a6f279d4ac441515f",
+ "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f",
"shasum": ""
},
"require": {
@@ -7508,7 +7564,7 @@
"type"
],
"support": {
- "source": "https://github.com/symfony/type-info/tree/v7.1.1"
+ "source": "https://github.com/symfony/type-info/tree/v7.1.5"
},
"funding": [
{
@@ -7524,7 +7580,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:59:31+00:00"
+ "time": "2024-09-19T21:48:23+00:00"
},
{
"name": "symfony/var-exporter",
@@ -7604,16 +7660,16 @@
},
{
"name": "symfony/yaml",
- "version": "v7.1.4",
+ "version": "v7.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "92e080b851c1c655c786a2da77f188f2dccd0f4b"
+ "reference": "4e561c316e135e053bd758bf3b3eb291d9919de4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/92e080b851c1c655c786a2da77f188f2dccd0f4b",
- "reference": "92e080b851c1c655c786a2da77f188f2dccd0f4b",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/4e561c316e135e053bd758bf3b3eb291d9919de4",
+ "reference": "4e561c316e135e053bd758bf3b3eb291d9919de4",
"shasum": ""
},
"require": {
@@ -7655,7 +7711,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.1.4"
+ "source": "https://github.com/symfony/yaml/tree/v7.1.5"
},
"funding": [
{
@@ -7671,7 +7727,7 @@
"type": "tidelift"
}
],
- "time": "2024-08-12T09:59:40+00:00"
+ "time": "2024-09-17T12:49:58+00:00"
},
{
"name": "tracy/tracy",
@@ -7747,6 +7803,87 @@
"source": "https://github.com/nette/tracy/tree/v2.10.8"
},
"time": "2024-08-07T02:04:53+00:00"
+ },
+ {
+ "name": "zircote/swagger-php",
+ "version": "4.10.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zircote/swagger-php.git",
+ "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e462ff5269ea0ec91070edd5d51dc7215bdea3b6",
+ "reference": "e462ff5269ea0ec91070edd5d51dc7215bdea3b6",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=7.2",
+ "psr/log": "^1.1 || ^2.0 || ^3.0",
+ "symfony/deprecation-contracts": "^2 || ^3",
+ "symfony/finder": ">=2.2",
+ "symfony/yaml": ">=3.3"
+ },
+ "require-dev": {
+ "composer/package-versions-deprecated": "^1.11",
+ "doctrine/annotations": "^1.7 || ^2.0",
+ "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1",
+ "phpstan/phpstan": "^1.6",
+ "phpunit/phpunit": ">=8",
+ "vimeo/psalm": "^4.23"
+ },
+ "suggest": {
+ "doctrine/annotations": "^1.7 || ^2.0"
+ },
+ "bin": [
+ "bin/openapi"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OpenApi\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Robert Allen",
+ "email": "zircote@gmail.com"
+ },
+ {
+ "name": "Bob Fanger",
+ "email": "bfanger@gmail.com",
+ "homepage": "https://bfanger.nl"
+ },
+ {
+ "name": "Martin Rademacher",
+ "email": "mano@radebatz.net",
+ "homepage": "https://radebatz.net"
+ }
+ ],
+ "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations",
+ "homepage": "https://github.com/zircote/swagger-php/",
+ "keywords": [
+ "api",
+ "json",
+ "rest",
+ "service discovery"
+ ],
+ "support": {
+ "issues": "https://github.com/zircote/swagger-php/issues",
+ "source": "https://github.com/zircote/swagger-php/tree/4.10.6"
+ },
+ "time": "2024-07-26T03:04:43+00:00"
}
],
"packages-dev": [
@@ -8013,16 +8150,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.12.1",
+ "version": "1.12.5",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "d8ed7fffa66de1db0d2972267d8ed1d8fa0fe5a2"
+ "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d8ed7fffa66de1db0d2972267d8ed1d8fa0fe5a2",
- "reference": "d8ed7fffa66de1db0d2972267d8ed1d8fa0fe5a2",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
+ "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
"shasum": ""
},
"require": {
@@ -8067,7 +8204,7 @@
"type": "github"
}
],
- "time": "2024-09-03T19:55:22+00:00"
+ "time": "2024-09-26T12:45:22+00:00"
},
{
"name": "phpstan/phpstan-nette",
diff --git a/generate-swagger b/generate-swagger
new file mode 100644
index 000000000..834017705
--- /dev/null
+++ b/generate-swagger
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+./bin/console swagger:annotate
+./cleaner
+./bin/console swagger:generate