diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf28bc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +vendor +composer.lock +.php-cs-fixer.cache \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..f4d3cbc --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +preset: laravel + +disabled: + - single_class_element_per_statement diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..06f5c25 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `laravel-api-factory` will be documented in this file + +## 1.0.0 - 04 Jan, 2023 + +- initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..be3b3b8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Danila Minustin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ab74e6 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# All in one Laravel API Factory + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/dminustin/laravel-api-factory.svg?style=flat-square)](https://packagist.org/packages/dminustin/laravel-api-factory) + +This package helps to fast generate APIs + +It makes: +- Controllers +- Actions +- Routes +- Swagger Documentation + +### The general philosophy is: + +"Actions" contains all functionality, you may want to use it without HTTP requests, for example, in workers + +"Controllers" must call "Actions" for any actions and must return an action result. No logic in controllers + +Default Directory structure: +``` +app +| http + | ApiFactory + | Actions + | Controllers +``` + +Controllers extends the ApiFactoryController + +Actions extends the ApiFactoryAction + +You can chane parent classes in stubs + + +## Todo +- implement middlewares in routes +- extend controller stub +- add postman collection writer + +## Installation + +You can install the package via composer: + +```bash +composer require dminustin/laravel-api-factory +``` + +## Swagger documentation +```bash +composer require --dev DarkaOnLine/L5-Swagger +``` + +API Documentation will be available at /api/documentation +``` +http://127.0.0.1:8000/api/documentation +``` + +## Configure +```bash +php artisan vendor:publish --tag=api-factory +``` + +Change the configuration file config/api-factory.php + +You can change ./stubs files +- api_factory_action +- api_factory_controller +- api_factory_router + +## Usage + +```php +php artisan api:factory +``` + +### Testing + +```bash +composer test +``` + +### Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c3f7424 --- /dev/null +++ b/composer.json @@ -0,0 +1,60 @@ +{ + "name": "dminustin/laravel-api-factory", + "description": "All-in-one", + "version": "1.0.0.0", + "keywords": [ + "dminustin", + "laravel-api-factory" + ], + "homepage": "https://github.com/dminustin/laravel-api-factory", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Danila Minustin", + "email": "danila.minustin@gmail.com", + "role": "Developer" + } + ], + "require": { + "php": "^7.4|^8.0", + "illuminate/support": ">=5.1", + "illuminate/database": ">=5.1", + "illuminate/contracts": ">=5.1", + "illuminate/filesystem": ">=5.1", + "illuminate/console": ">=5.1", + "symfony/yaml": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "dev-master" + }, + "autoload": { + "psr-4": { + "Dminustin\\ApiFactory\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Dminustin\\ApiFactory\\Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Dminustin\\ApiFactory\\LaravelApiFactoryServiceProvider" + ], + "aliases": { + "ApiFactory": "Dminustin\\ApiFactory\\LaravelApiFactoryFacade" + } + } + } +} diff --git a/config/api-factory.php b/config/api-factory.php new file mode 100644 index 0000000..e277cec --- /dev/null +++ b/config/api-factory.php @@ -0,0 +1,51 @@ + true, + 'generateControllers' => true, + 'generateActions' => true, + 'generateSwaggerDoc' => true, + 'overrideControllers' => false, + 'overrideActions' => false, + 'routesFile' => 'app/routes/example.yaml', + + 'outRouteFileName' => 'routes/web.php', + + 'uriPrefix' => '/api/v1/', + + 'generatedControllerPathPrefix' => 'app/Http/ApiFactory/Controllers', + 'generatedActionPathPrefix' => 'app/Http/ApiFactory/Actions', + + 'generatedControllerNSPrefix' => 'App/Http/ApiFactory/Controllers', + 'generatedActionNSPrefix' => 'App/Http/ApiFactory/Actions', + + 'generatedControllerNSSuffix' => 'Controller', + 'generatedActionNSSuffix' => 'Action', + + + 'middlewares' => [ + 'auth' => ['auth', 'registered'], + 'guest' => ['guest'], + 'admin' => ['toleAdmin'] + ], + + //Swagger response definitions + 'definitions' => [ + 'baseResponse' => [ + 'type' => 'object', + 'required' => [ + 'result', + 'data' + ], + 'properties' => [ + 'result' => [ + 'type' => 'boolean', + ], + 'data' => [ + 'type' => 'object' + ] + + ] + ] + ] +]; \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..b2ffd63 --- /dev/null +++ b/makefile @@ -0,0 +1,4 @@ +fix: + ./vendor/bin/php-cs-fixer fix ./src +test: + ./vendor/bin/phpunit tests \ No newline at end of file diff --git a/routes/example.yaml b/routes/example.yaml new file mode 100644 index 0000000..2ed409b --- /dev/null +++ b/routes/example.yaml @@ -0,0 +1,51 @@ +auth/: + - + path: login + method: post + description: Login into system + middlewares: [guest] + produces: + - application/json + tags: [auth, user] + params: + email: string|required|unique:users,email + password: string|required + responses: + 200: + description: Base response + schema: + items: + "$ref": "#/definitions/baseResponse" + - + path: register + middlewares: [guest] + description: User registraction + tags: [auth, user] + produces: + - application/json + method: post + params: + email: string|required + password: string|required + confirm: boolean|required + - + path: reset + middlewares: [guest] + tags: [auth, user] + method: post + params: + email: string|required +user/avatar/: + - + tags: [avatar, user] + path: get + method: get + - + path: update + middlewares: [registered] + headers: + authorization: string|required + tags: [avatar, user] + method: post + params: + avatar: file|required \ No newline at end of file diff --git a/src/Classes/AbstractClasses/AbstractDataConverter.php b/src/Classes/AbstractClasses/AbstractDataConverter.php new file mode 100644 index 0000000..f3d1ef4 --- /dev/null +++ b/src/Classes/AbstractClasses/AbstractDataConverter.php @@ -0,0 +1,15 @@ +data = $data; + } + + abstract public function convert(): mixed; +} diff --git a/src/Classes/AbstractClasses/AbstractGenerator.php b/src/Classes/AbstractClasses/AbstractGenerator.php new file mode 100644 index 0000000..20f77a2 --- /dev/null +++ b/src/Classes/AbstractClasses/AbstractGenerator.php @@ -0,0 +1,50 @@ +config = $config; + $this->routes = $routes; + $this->log = $log; + } + + abstract public function generate(); + + protected function loadTemplate(string $templateName): string + { + return file_get_contents(base_path('stubs/' . $templateName)); + } + + protected function writeFile(string $rendered, string $outName) + { + $dir = dirname($outName); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + + file_put_contents($outName, $rendered); + } + + protected function render(string $template): string + { + foreach ($this->renderingMap as $key => $value) { + $template = str_replace('%' . $key . '%', $value, $template); + } + return $template; + } +} diff --git a/src/Classes/ApiFactoryAction.php b/src/Classes/ApiFactoryAction.php new file mode 100644 index 0000000..484c9a1 --- /dev/null +++ b/src/Classes/ApiFactoryAction.php @@ -0,0 +1,17 @@ +validate($this->validationRules); + $this->data = $request->safe()->only($this->validationRules); + } +} diff --git a/src/Classes/ApiFactoryController.php b/src/Classes/ApiFactoryController.php new file mode 100644 index 0000000..982ea00 --- /dev/null +++ b/src/Classes/ApiFactoryController.php @@ -0,0 +1,15 @@ +actionName(); + } +} diff --git a/src/Classes/BaseModel.php b/src/Classes/BaseModel.php new file mode 100644 index 0000000..7a7bd8d --- /dev/null +++ b/src/Classes/BaseModel.php @@ -0,0 +1,48 @@ + $value) { + $this->$key = $value; + } + } + + /** + * @throws Exception + */ + public function __get($key) + { + return $this->checkProperty($key)->$key; + } + + public function __set($key, $value) + { + $this->checkProperty($key)->$key = $value; + } + + /** + * @throws Exception + */ + protected function checkProperty($key) + { + if (!property_exists($this, $key)) { + throw new Exception('Unable to get property ' . $key); + } + return $this; + } + + public function toArray(): array + { + $result = []; + foreach (get_object_vars($this) as $key => $val) { + $result[$key] = $val; + } + return $result; + } +} diff --git a/src/Classes/ClassAttributes.php b/src/Classes/ClassAttributes.php new file mode 100644 index 0000000..5c7f697 --- /dev/null +++ b/src/Classes/ClassAttributes.php @@ -0,0 +1,97 @@ +uri = $uri; + $this->pathPrefix = $pathPrefix; + $this->nsPrefix = $nsPrefix; + $this->nsSuffix = $nsSuffix; + + $this->calcNameSpace(); + $this->calcClassName(); + $this->calcFilePath(); + } + + protected function calcNameSpace() + { + $tmp = explode('/', $this->formatName()); + array_pop($tmp); + $this->nameSpace = implode('\\', $tmp); + } + + protected function formatName() + { + $result = sprintf( + '%s/%s%s', + (!empty($this->nsPrefix)) ? $this->nsPrefix . '/' : '', + ucwords($this->uri, '/'), + (!empty($this->nsSuffix)) ? $this->nsSuffix : '', + ); + return preg_replace('#[/]+#', '/', $result); + } + + protected function calcClassName() + { + $this->className = str_replace('/', '\\', $this->formatName()); + } + + protected function calcFilePath() + { + $result = sprintf( + '%s/%s%s', + (!empty($this->pathPrefix)) ? $this->pathPrefix . '/' : '', + ucwords($this->uri, '/'), + (!empty($this->nsSuffix)) ? $this->nsSuffix : '', + ); + $this->filePath = base_path(preg_replace('#[/]+#', '/', $result) . '.php'); + } + + /** + * @return string + */ + public function getNameSpace(): string + { + return $this->nameSpace; + } + + /** + * @return string + */ + public function getClassName(): string + { + return $this->className; + } + + /** + * @return string + */ + public function getShortClassName(): string + { + return last(explode('\\', $this->className)); + } + + /** + * @return string + */ + public function getFilePath(): string + { + return $this->filePath; + } +} diff --git a/src/Classes/Config.php b/src/Classes/Config.php new file mode 100644 index 0000000..860de78 --- /dev/null +++ b/src/Classes/Config.php @@ -0,0 +1,53 @@ + ['type' => 'boolean'], + 'bool' => ['type' => 'boolean'], + 'file' => ['type' => 'string', 'format' => 'binary'], + 'string' => ['type' => 'string'], + 'integer' => ['type' => 'integer', 'format' => 'int64'], + ]; + + public function __construct($data) + { + $this->data = explode('|', $data); + } + + public function convert(): SwaggerRule + { + $result = new SwaggerRule([]); + $result->required = (in_array('required', $this->data)); + foreach ($this->typesMap as $type => $attributes) { + if (in_array($type, $this->data)) { + $result->type = $attributes['type']; + if (isset($attributes['format'])) { + $result->format = $attributes['format']; + } + } + } + return $result; + } +} diff --git a/src/Classes/DataConverters/SwaggerParamsConverter.php b/src/Classes/DataConverters/SwaggerParamsConverter.php new file mode 100644 index 0000000..992e274 --- /dev/null +++ b/src/Classes/DataConverters/SwaggerParamsConverter.php @@ -0,0 +1,35 @@ +data as $name => $rules) { + $result[] = + array_merge( + ['name' => $name], + (new RuleConverter($rules))->convert() + ->setIn($this->in) + ->toArray() + ); + } + return $result; + } + + /** + * @param string $in + * @return static + */ + public function setIn(string $in): static + { + $this->in = $in; + return $this; + } +} diff --git a/src/Classes/Generators/HttpActionGenerator.php b/src/Classes/Generators/HttpActionGenerator.php new file mode 100644 index 0000000..e15c6c8 --- /dev/null +++ b/src/Classes/Generators/HttpActionGenerator.php @@ -0,0 +1,62 @@ +loadTemplate('api_factory_action'); + + /** + * @var EndPoint $route + */ + foreach ($this->routes as $route) { + $actionAttributes = new ClassAttributes( + $route->path, + $this->config->generatedActionPathPrefix, + $this->config->generatedActionNSPrefix, + $this->config->generatedActionNSSuffix, + ); + + if (!$this->config->overrideControllers && file_exists($actionAttributes->getFilePath())) { + $this->log->warning('Action exists: ' . json_encode([ + 'Path:' => $route->path, + 'Filename:' => $actionAttributes->getFilePath(), + 'ClassName:' => $actionAttributes->getClassName(), + 'ShortName:' => $actionAttributes->getShortClassName(), + ])); + continue; + } + + $this->log->info('Generate Action ' . json_encode([ + 'Path:' => $route->path, + 'Filename:' => $actionAttributes->getFilePath(), + 'ClassName:' => $actionAttributes->getClassName(), + 'ShortName:' => $actionAttributes->getShortClassName(), + ])); + + $rules = ['[']; + foreach ($route->params as $key => $value) { + $rules[] = ' \'' . $key . '\'=>\'' . + $value + . '\','; + } + $rules[] = ' ]'; + + $this->renderingMap = [ + 'namespace' => $actionAttributes->getNameSpace(), + 'className' => $actionAttributes->getShortClassName(), + 'actionName' => $actionAttributes->getClassName(), + 'rules' => implode(PHP_EOL, $rules), + ]; + + $rendered = $this->render($template); + $this->writeFile($rendered, $actionAttributes->getFilePath()); + } + } +} diff --git a/src/Classes/Generators/HttpControllerGenerator.php b/src/Classes/Generators/HttpControllerGenerator.php new file mode 100644 index 0000000..1da0900 --- /dev/null +++ b/src/Classes/Generators/HttpControllerGenerator.php @@ -0,0 +1,59 @@ +loadTemplate('api_factory_controller'); + + /** + * @var EndPoint $route + */ + foreach ($this->routes as $route) { + $classAttributes = new ClassAttributes( + $route->path, + $this->config->generatedControllerPathPrefix, + $this->config->generatedControllerNSPrefix, + $this->config->generatedControllerNSSuffix, + ); + $actionAttributes = new ClassAttributes( + $route->path, + $this->config->generatedActionPathPrefix, + $this->config->generatedActionNSPrefix, + $this->config->generatedActionNSSuffix, + ); + + if (!$this->config->overrideControllers && file_exists($classAttributes->getFilePath())) { + $this->log->warning('Class exists: ' . json_encode([ + 'Path:' => $route->path, + 'Filename:' => $classAttributes->getFilePath(), + 'ClassName:' => $classAttributes->getClassName(), + 'ShortName:' => $classAttributes->getShortClassName(), + ])); + continue; + } + + $this->log->info('Generate Controller ' . json_encode([ + 'Path:' => $route->path, + 'Filename:' => $classAttributes->getFilePath(), + 'ClassName:' => $classAttributes->getClassName(), + 'ShortName:' => $classAttributes->getShortClassName(), + ])); + + $this->renderingMap = [ + 'namespace' => $classAttributes->getNameSpace(), + 'className' => $classAttributes->getShortClassName(), + 'actionName' => $actionAttributes->getClassName(), + ]; + + $rendered = $this->render($template); + $this->writeFile($rendered, $classAttributes->getFilePath()); + } + } +} diff --git a/src/Classes/Generators/RouterGenerator.php b/src/Classes/Generators/RouterGenerator.php new file mode 100644 index 0000000..bff9b48 --- /dev/null +++ b/src/Classes/Generators/RouterGenerator.php @@ -0,0 +1,51 @@ +loadTemplate('api_factory_router'); + + $result = []; + /** + * @var EndPoint $route + */ + foreach ($this->routes as $route) { + $classAttributes = new ClassAttributes( + $route->path, + $this->config->generatedControllerPathPrefix, + $this->config->generatedControllerNSPrefix, + $this->config->generatedControllerNSSuffix, + ); + $this->renderingMap = [ + 'namespace' => $classAttributes->getNameSpace(), + 'className' => $classAttributes->getClassName(), + 'method' => $route->method, + 'path' => $route->path, + ]; + $result[] = $this->render($template); + } + + $fName = base_path($this->config->outRouteFileName); + $file = file_get_contents($fName); + $file = explode(static::DIVIDER, $file, 2); + if (count($file) === 1) { + $file[] = ''; + } + $file[0] = trim($file[0]); + $file[1] = implode(PHP_EOL, $result); + file_put_contents($fName, implode(static::DIVIDER, $file)); + } +} diff --git a/src/Classes/Generators/SwaggerGenerator.php b/src/Classes/Generators/SwaggerGenerator.php new file mode 100644 index 0000000..d47dad9 --- /dev/null +++ b/src/Classes/Generators/SwaggerGenerator.php @@ -0,0 +1,70 @@ + '2.0', + 'info' => [ + 'title' => config('app.title'), + 'version' => '1.0', + 'description' => 'Generated by Api Factory', + ], + 'host' => config('app.url'), + 'basePath' => '/api', + 'schemes' => [ + 'http' + ], + 'consumes' => [ + 'application/json' + ], + 'produces' => [ + 'application/json' + ], + 'paths' => [] + ]; + /** @var EndPoint $route */ + foreach ($this->routes as $route) { + $params = []; + if (count($route->params)) { + $params = (new SwaggerParamsConverter($route->params)) + ->setIn('query') + ->convert(); + } + if (count($route->headers)) { + $params = array_merge( + $params, + (new SwaggerParamsConverter($route->headers)) + ->setIn('headers') + ->convert() + ); + } + $result['paths'][$route->path][$route->method] = + [ + 'description' => $route->description, + 'produces' => $route->produces, + 'responses' => $route->responses, + 'parameters' => $params, + 'tags' => $route->tags, + ]; + } + $result['definitions'] = $this->config->definitions; + $result['componentss'] = $this->config->components; + + $outFileName = base_path('public/docs/api-docs.json'); + if (!is_dir(dirname($outFileName))) { + mkdir(dirname($outFileName), 0777, true); + } + file_put_contents($outFileName, json_encode($result, JSON_PRETTY_PRINT)); + } +} diff --git a/src/Classes/Logger.php b/src/Classes/Logger.php new file mode 100644 index 0000000..c029c21 --- /dev/null +++ b/src/Classes/Logger.php @@ -0,0 +1,43 @@ +formatGreen($message); + } elseif ($level == 'warning') { + $message = $this->formatYellow($message); + } else { + $message = $this->formatGreen($message); + } + $messageFormatted = sprintf( + '%s: %s', + $this->formatYellowBG($level), + $message + ); + echo $messageFormatted . PHP_EOL; + } + + protected function formatGreen($message): string + { + return "\033[92m" . $message . "\033[0m"; + } + + protected function formatYellow($message): string + { + return "\033[33m" . $message . "\033[0m"; + } + + protected function formatYellowBG($message): string + { + return "\033[1;103m" . $message . "\033[0m"; + } + + protected function formatRed($message): string + { + return "\033[91m" . $message . "\033[0m"; + } +} diff --git a/src/Classes/RouterConfig/EndPoint.php b/src/Classes/RouterConfig/EndPoint.php new file mode 100644 index 0000000..2d014b4 --- /dev/null +++ b/src/Classes/RouterConfig/EndPoint.php @@ -0,0 +1,34 @@ + [ + 'schema' => ['$ref' => '#/definitions/baseResponse'] + ] + ]; + protected array $tags = []; + protected array $headers = []; + protected array $produces = ['application/json']; + protected array $middlewares = []; + protected array $params = []; +} diff --git a/src/Classes/RouterConfig/EndPointCollection.php b/src/Classes/RouterConfig/EndPointCollection.php new file mode 100644 index 0000000..f06504a --- /dev/null +++ b/src/Classes/RouterConfig/EndPointCollection.php @@ -0,0 +1,55 @@ + $endPoints) { + foreach ($endPoints as $endPoint) { + $this->addEndPoint($prefix, $endPoint); + } + } + } + + public function addEndPoint(string $prefix, array $endpoint): static + { + $point = new EndPoint($endpoint); + $point->path = $prefix . $point->path; + $this->endPoints[] = $point; + return $this; + } + + public function getEndPoints(): array + { + return $this->endPoints; + } + + public function toArray() + { + $paths = []; + foreach ($this->endPoints as $point) { + $points[] = $point->toArray(); + $path = explode('/', $point->path); + if (count($path) > 1) { + array_pop($path); + $key = implode('/', $path) . '/'; + } else { + $key = $point->path; + } + $thisPoint = $point->toArray(); + $thisPoint['path'] = explode($key, $thisPoint['path'], 2); + array_shift($thisPoint['path']); + $thisPoint['path'] = implode('', $thisPoint['path']); + if (!isset($paths[$key])) { + $paths[$key] = [$thisPoint]; + } else { + $paths[$key][] = $thisPoint; + } + } + return $paths; + } +} diff --git a/src/Classes/RouterConfig/Responses.php b/src/Classes/RouterConfig/Responses.php new file mode 100644 index 0000000..e73f369 --- /dev/null +++ b/src/Classes/RouterConfig/Responses.php @@ -0,0 +1,9 @@ +in = $in; + return $this; + } +} diff --git a/src/Console/ApiCreateCommand.php b/src/Console/ApiCreateCommand.php new file mode 100644 index 0000000..b24bcc9 --- /dev/null +++ b/src/Console/ApiCreateCommand.php @@ -0,0 +1,97 @@ + []]; + foreach ($config->middlewares as $key => $vals) { + $mw[] = $key . ': ' . implode(', ', $vals); + $middlewares[$key] = $vals; + } + + while (empty($newRoute['path'])) { + $newRoute['path'] = $this->ask('URI address without uriPrefix, for example: auth/login', ''); + } + while (empty($newRoute['method'])) { + $newRoute['method'] = $this->choice('Request method', ['post', 'get', 'put', 'delete', 'patch', 'any'], 'post'); + } + + /** @var EndPoint $route */ + foreach ($routes->getEndPoints() as $route) { + if ( + $route->path === $newRoute['path'] && + $route->method === $newRoute['method'] + ) { + $this->stopApp(sprintf('Route %s : %s already exists', $newRoute['method'], $newRoute['path'])); + } + } + + while (empty($newRoute['middlewares'])) { + $newRoute['middlewares'] = $this->choice('Middlewares', $mw, 'empty'); + } + + $newRoute['middlewares'] = $middlewares[$newRoute['middlewares']]; + + $newRoute['params'] = []; + for (; ;) { + $newParam = $this->ask('Param name', ''); + if (empty($newParam)) { + $stop = $this->choice('Stop asking params', ['yes', 'no'], 'yes'); + if ($stop === 'yes') { + break; + } + continue; + } + $newParamAttribute = $this->ask('Param attributes (required, nullable etc)', ''); + $newRoute['params'][$newParam] = $newParamAttribute; + } + + $routes->addEndPoint('', $newRoute); + + $ep = sprintf( + "URI: %s,\nMethod: %s,\nMiddleware: %s,\nParams: %s", + $newRoute['path'], + $newRoute['method'], + var_export($newRoute['middlewares'], true), + var_export($newRoute['params'], true) + ); + + $this->choice('Confirm to create endpoint: ' . $ep, ['yes', 'no']); + + file_put_contents(base_path($config->routesFile), Yaml::dump($routes->toArray())); + } + + protected function stopApp($message) + { + $this->warn($message); + exit(0); + } +} diff --git a/src/Console/ApiFactoryCommand.php b/src/Console/ApiFactoryCommand.php new file mode 100644 index 0000000..9975a6b --- /dev/null +++ b/src/Console/ApiFactoryCommand.php @@ -0,0 +1,39 @@ +apiFactory = $apiFactory; + } + + + public function handle() + { + $routes = new EndPointCollection( + Yaml::parseFile( + base_path( + config('api-factory')['routesFile'] + ) + ) + ); + + /**@var AbstractGenerator $generator */ + foreach ($this->apiFactory->getGenerators($routes) as $generator) { + $generator->generate(); + } + } +} diff --git a/src/LaravelApiFactory.php b/src/LaravelApiFactory.php new file mode 100644 index 0000000..4713407 --- /dev/null +++ b/src/LaravelApiFactory.php @@ -0,0 +1,51 @@ + 'generateRoutes', + HttpControllerGenerator::class => 'generateControllers', + HttpActionGenerator::class => 'generateActions', + SwaggerGenerator::class => 'generateSwaggerDoc', + ]; + + protected Config $config; + protected Logger $logger; + + public function __construct(Logger $logger) + { + $this->logger = $logger; + $this->setConfig(new Config(config('api-factory'))); + } + + /** + * @param Config $config + * @return static + */ + public function setConfig(Config $config): static + { + $this->config = $config; + return $this; + } + + public function getGenerators(EndPointCollection $routes): array + { + $result = []; + foreach ($this->generators as $generator => $flag) { + if ($this->config->$flag === true) { + $result[] = new $generator($this->config, $routes->getEndPoints(), $this->logger); + } + } + return $result; + } +} diff --git a/src/LaravelApiFactoryServiceProvider.php b/src/LaravelApiFactoryServiceProvider.php new file mode 100644 index 0000000..7a7290e --- /dev/null +++ b/src/LaravelApiFactoryServiceProvider.php @@ -0,0 +1,43 @@ +app->runningInConsole()) { + $this->publishes([ + __DIR__ . '/../config/api-factory.php' => config_path('api-factory.php'), + __DIR__ . '/../routes/example.yaml' => base_path('app/routes/example.yaml'), + __DIR__ . '/../stubs/api_factory_controller' => base_path('stubs/api_factory_controller'), + __DIR__ . '/../stubs/api_factory_action' => base_path('stubs/api_factory_action'), + __DIR__ . '/../stubs/api_factory_router' => base_path('stubs/api_factory_router') + ], 'api-factory'); + + $this->commands([ + ApiFactoryCommand::class, + ApiCreateCommand::class, + ]); + } + } + + /** + * Register the application services. + */ + public function register() + { + $this->mergeConfigFrom(__DIR__ . '/../config/api-factory.php', 'api-factory'); + + $this->app->singleton('api-factory', function () { + return new LaravelApiFactory(); + }); + } +} diff --git a/stubs/api_factory_action b/stubs/api_factory_action new file mode 100644 index 0000000..88dd067 --- /dev/null +++ b/stubs/api_factory_action @@ -0,0 +1,17 @@ +checkAndFillRequestData($request); + //do something with $this-data + } +} \ No newline at end of file diff --git a/stubs/api_factory_controller b/stubs/api_factory_controller new file mode 100644 index 0000000..ab79562 --- /dev/null +++ b/stubs/api_factory_controller @@ -0,0 +1,16 @@ +