123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678 |
- <?php
- namespace Mpociot\ApiDoc\Generators;
- use Faker\Factory;
- use ReflectionClass;
- use Illuminate\Support\Arr;
- use Illuminate\Support\Str;
- use Mpociot\Reflection\DocBlock;
- use Mpociot\Reflection\DocBlock\Tag;
- use Illuminate\Support\Facades\Validator;
- use Illuminate\Foundation\Http\FormRequest;
- use Mpociot\ApiDoc\Parsers\RuleDescriptionParser as Description;
- use Illuminate\Contracts\Validation\Factory as ValidationFactory;
- abstract class AbstractGenerator
- {
- /**
- * @param $route
- *
- * @return mixed
- */
- abstract public function getDomain($route);
- /**
- * @param $route
- *
- * @return mixed
- */
- abstract public function getUri($route);
- /**
- * @param $route
- *
- * @return mixed
- */
- abstract public function getMethods($route);
- /**
- * @param \Illuminate\Routing\Route $route
- * @param array $bindings
- * @param bool $withResponse
- *
- * @return array
- */
- public function processRoute($route, $bindings = [], $headers = [], $withResponse = true)
- {
- $routeDomain = $route->domain();
- $routeAction = $route->getAction();
- $routeGroup = $this->getRouteGroup($routeAction['uses']);
- $routeDescription = $this->getRouteDescription($routeAction['uses']);
- $showresponse = null;
- // set correct route domain
- $headers[] = "HTTP_HOST: {$routeDomain}";
- $headers[] = "SERVER_NAME: {$routeDomain}";
- $response = null;
- $docblockResponse = $this->getDocblockResponse($routeDescription['tags']);
- if ($docblockResponse) {
- // we have a response from the docblock ( @response )
- $response = $docblockResponse;
- $showresponse = true;
- }
- if (! $response) {
- $transformerResponse = $this->getTransformerResponse($routeDescription['tags']);
- if ($transformerResponse) {
- // we have a transformer response from the docblock ( @transformer || @transformercollection )
- $response = $transformerResponse;
- $showresponse = true;
- }
- }
- if (! $response && $withResponse) {
- try {
- $response = $this->getRouteResponse($route, $bindings, $headers);
- } catch (\Exception $e) {
- dump("Couldn't get response for route: ".implode(',', $this->getMethods($route)).'] '.$route->uri().'', $e);
- }
- }
- $content = $this->getResponseContent($response);
- return $this->getParameters([
- 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
- 'resource' => $routeGroup,
- 'title' => $routeDescription['short'],
- 'description' => $routeDescription['long'],
- 'methods' => $this->getMethods($route),
- 'uri' => $this->getUri($route),
- 'parameters' => [],
- 'response' => $content,
- 'showresponse' => $showresponse,
- ], $routeAction, $bindings);
- }
- /**
- * Prepares / Disables route middlewares.
- *
- * @param bool $disable
- *
- * @return void
- */
- abstract public function prepareMiddleware($enable = false);
- /**
- * Get the response from the docblock if available.
- *
- * @param array $tags
- *
- * @return mixed
- */
- protected function getDocblockResponse($tags)
- {
- $responseTags = array_filter($tags, function ($tag) {
- if (! ($tag instanceof Tag)) {
- return false;
- }
- return \strtolower($tag->getName()) == 'response';
- });
- if (empty($responseTags)) {
- return;
- }
- $responseTag = \array_first($responseTags);
- return \response(json_encode($responseTag->getContent()), 200, ['Content-Type' => 'application/json']);
- }
- /**
- * @param array $routeData
- * @param array $routeAction
- * @param array $bindings
- *
- * @return mixed
- */
- protected function getParameters($routeData, $routeAction, $bindings)
- {
- $rules = $this->simplifyRules($this->getRouteRules($routeData['methods'], $routeAction['uses'], $bindings));
- foreach ($rules as $attribute => $ruleset) {
- $attributeData = [
- 'required' => false,
- 'type' => null,
- 'default' => '',
- 'value' => '',
- 'description' => [],
- ];
- foreach ($ruleset as $rule) {
- $this->parseRule($rule, $attribute, $attributeData, $routeData['id'], $routeData);
- }
- $routeData['parameters'][$attribute] = $attributeData;
- }
- return $routeData;
- }
- /**
- * Format the validation rules as plain array.
- *
- * @param array $rules
- *
- * @return array
- */
- protected function simplifyRules($rules)
- {
- $simplifiedRules = Validator::make([], $rules)->getRules();
- if (count($simplifiedRules) === 0) {
- return $simplifiedRules;
- }
- $values = collect($simplifiedRules)
- ->filter(function ($values) {
- return in_array('array', $values);
- })->map(function ($val, $key) {
- return [''];
- })->all();
- return Validator::make($values, $rules)->getRules();
- }
- /**
- * @param $route
- * @param $bindings
- * @param $headers
- *
- * @return \Illuminate\Http\Response
- */
- protected function getRouteResponse($route, $bindings, $headers = [])
- {
- $uri = $this->addRouteModelBindings($route, $bindings);
- $methods = $this->getMethods($route);
- // Split headers into key - value pairs
- $headers = collect($headers)->map(function ($value) {
- $split = explode(':', $value); // explode to get key + values
- $key = array_shift($split); // extract the key and keep the values in the array
- $value = implode(':', $split); // implode values into string again
- return [trim($key) => trim($value)];
- })->collapse()->toArray();
- //Changes url with parameters like /users/{user} to /users/1
- $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters
- return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers);
- }
- /**
- * @param $route
- * @param array $bindings
- *
- * @return mixed
- */
- protected function addRouteModelBindings($route, $bindings)
- {
- $uri = $this->getUri($route);
- foreach ($bindings as $model => $id) {
- $uri = str_replace('{'.$model.'}', $id, $uri);
- $uri = str_replace('{'.$model.'?}', $id, $uri);
- }
- return $uri;
- }
- /**
- * @param \Illuminate\Routing\Route $route
- *
- * @return array
- */
- protected function getRouteDescription($route)
- {
- list($class, $method) = explode('@', $route);
- $reflection = new ReflectionClass($class);
- $reflectionMethod = $reflection->getMethod($method);
- $comment = $reflectionMethod->getDocComment();
- $phpdoc = new DocBlock($comment);
- return [
- 'short' => $phpdoc->getShortDescription(),
- 'long' => $phpdoc->getLongDescription()->getContents(),
- 'tags' => $phpdoc->getTags(),
- ];
- }
- /**
- * @param string $route
- *
- * @return string
- */
- protected function getRouteGroup($route)
- {
- list($class, $method) = explode('@', $route);
- $reflection = new ReflectionClass($class);
- $comment = $reflection->getDocComment();
- if ($comment) {
- $phpdoc = new DocBlock($comment);
- foreach ($phpdoc->getTags() as $tag) {
- if ($tag->getName() === 'resource') {
- return $tag->getContent();
- }
- }
- }
- return 'general';
- }
- /**
- * @param array $routeMethods
- * @param string $routeAction
- * @param array $bindings
- *
- * @return array
- */
- protected function getRouteRules(array $routeMethods, $routeAction, $bindings)
- {
- list($class, $method) = explode('@', $routeAction);
- $reflection = new ReflectionClass($class);
- $reflectionMethod = $reflection->getMethod($method);
- foreach ($reflectionMethod->getParameters() as $parameter) {
- $parameterType = $parameter->getClass();
- if (! is_null($parameterType) && class_exists($parameterType->name)) {
- $className = $parameterType->name;
- if (is_subclass_of($className, FormRequest::class)) {
- /** @var FormRequest $formRequest */
- $formRequest = new $className;
- // Add route parameter bindings
- $formRequest->setContainer(app());
- $formRequest->request->add($bindings);
- $formRequest->query->add($bindings);
- $formRequest->setMethod($routeMethods[0]);
- if (method_exists($formRequest, 'validator')) {
- $factory = app(ValidationFactory::class);
- return call_user_func_array([$formRequest, 'validator'], [$factory])
- ->getRules();
- } else {
- return call_user_func_array([$formRequest, 'rules'], []);
- }
- }
- }
- }
- return [];
- }
- /**
- * @param array $arr
- * @param string $first
- * @param string $last
- *
- * @return string
- */
- protected function fancyImplode($arr, $first, $last)
- {
- $arr = array_map(function ($value) {
- return '`'.$value.'`';
- }, $arr);
- array_push($arr, implode($last, array_splice($arr, -2)));
- return implode($first, $arr);
- }
- protected function splitValuePairs($parameters, $first = 'is ', $last = 'or ')
- {
- $attribute = '';
- collect($parameters)->map(function ($item, $key) use (&$attribute, $first, $last) {
- $attribute .= '`'.$item.'` ';
- if (($key + 1) % 2 === 0) {
- $attribute .= $last;
- } else {
- $attribute .= $first;
- }
- });
- $attribute = rtrim($attribute, $last);
- return $attribute;
- }
- /**
- * @param string $rule
- * @param string $ruleName
- * @param array $attributeData
- * @param int $seed
- *
- * @return void
- */
- protected function parseRule($rule, $ruleName, &$attributeData, $seed, $routeData)
- {
- $faker = Factory::create();
- $faker->seed(crc32($seed));
- $parsedRule = $this->parseStringRule($rule);
- $parsedRule[0] = $this->normalizeRule($parsedRule[0]);
- list($rule, $parameters) = $parsedRule;
- switch ($rule) {
- case 'required':
- $attributeData['required'] = true;
- break;
- case 'accepted':
- $attributeData['required'] = true;
- $attributeData['type'] = 'boolean';
- $attributeData['value'] = true;
- break;
- case 'after':
- $attributeData['type'] = 'date';
- $format = isset($attributeData['format']) ? $attributeData['format'] : DATE_RFC850;
- if (strtotime($parameters[0]) === false) {
- // the `after` date refers to another parameter in the request
- $paramName = $parameters[0];
- $attributeData['description'][] = Description::parse($rule)->with($paramName)->getDescription();
- $attributeData['value'] = date($format, strtotime('+1 day', strtotime($routeData['parameters'][$paramName]['value'])));
- } else {
- $attributeData['description'][] = Description::parse($rule)->with(date($format, strtotime($parameters[0])))->getDescription();
- $attributeData['value'] = date($format, strtotime('+1 day', strtotime($parameters[0])));
- }
- break;
- case 'alpha':
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- $attributeData['value'] = $faker->word;
- break;
- case 'alpha_dash':
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- break;
- case 'alpha_num':
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- break;
- case 'in':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
- $attributeData['value'] = $faker->randomElement($parameters);
- break;
- case 'not_in':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
- $attributeData['value'] = $faker->word;
- break;
- case 'min':
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- if (Arr::get($attributeData, 'type') === 'numeric' || Arr::get($attributeData, 'type') === 'integer') {
- $attributeData['value'] = $faker->numberBetween($parameters[0]);
- }
- break;
- case 'max':
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- if (Arr::get($attributeData, 'type') === 'numeric' || Arr::get($attributeData, 'type') === 'integer') {
- $attributeData['value'] = $faker->numberBetween(0, $parameters[0]);
- }
- break;
- case 'between':
- if (! isset($attributeData['type'])) {
- $attributeData['type'] = 'numeric';
- }
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- $attributeData['value'] = $faker->numberBetween($parameters[0], $parameters[1]);
- break;
- case 'before':
- $attributeData['type'] = 'date';
- $format = isset($attributeData['format']) ? $attributeData['format'] : DATE_RFC850;
- if (strtotime($parameters[0]) === false) {
- // the `before` date refers to another parameter in the request
- $paramName = $parameters[0];
- $attributeData['description'][] = Description::parse($rule)->with($paramName)->getDescription();
- $attributeData['value'] = date($format, strtotime('-1 day', strtotime($routeData['parameters'][$paramName]['value'])));
- } else {
- $attributeData['description'][] = Description::parse($rule)->with(date($format, strtotime($parameters[0])))->getDescription();
- $attributeData['value'] = date($format, strtotime('-1 day', strtotime($parameters[0])));
- }
- break;
- case 'date_format':
- $attributeData['type'] = 'date';
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- $attributeData['format'] = $parameters[0];
- $attributeData['value'] = date($attributeData['format']);
- break;
- case 'different':
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- break;
- case 'digits':
- $attributeData['type'] = 'numeric';
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- $attributeData['value'] = ($parameters[0] < 9) ? $faker->randomNumber($parameters[0], true) : substr(mt_rand(100000000, mt_getrandmax()), 0, $parameters[0]);
- break;
- case 'digits_between':
- $attributeData['type'] = 'numeric';
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- break;
- case 'file':
- $attributeData['type'] = 'file';
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- break;
- case 'image':
- $attributeData['type'] = 'image';
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- break;
- case 'json':
- $attributeData['type'] = 'string';
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- $attributeData['value'] = json_encode(['foo', 'bar', 'baz']);
- break;
- case 'mimetypes':
- case 'mimes':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
- break;
- case 'required_if':
- $attributeData['description'][] = Description::parse($rule)->with($this->splitValuePairs($parameters))->getDescription();
- break;
- case 'required_unless':
- $attributeData['description'][] = Description::parse($rule)->with($this->splitValuePairs($parameters))->getDescription();
- break;
- case 'required_with':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
- break;
- case 'required_with_all':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' and '))->getDescription();
- break;
- case 'required_without':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
- break;
- case 'required_without_all':
- $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' and '))->getDescription();
- break;
- case 'same':
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- break;
- case 'size':
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- break;
- case 'timezone':
- $attributeData['description'][] = Description::parse($rule)->getDescription();
- $attributeData['value'] = $faker->timezone;
- break;
- case 'exists':
- $fieldName = isset($parameters[1]) ? $parameters[1] : $ruleName;
- $attributeData['description'][] = Description::parse($rule)->with([Str::singular($parameters[0]), $fieldName])->getDescription();
- break;
- case 'active_url':
- $attributeData['type'] = 'url';
- $attributeData['value'] = $faker->url;
- break;
- case 'regex':
- $attributeData['type'] = 'string';
- $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
- break;
- case 'boolean':
- $attributeData['value'] = true;
- $attributeData['type'] = $rule;
- break;
- case 'array':
- $attributeData['value'] = $faker->word;
- $attributeData['type'] = $rule;
- break;
- case 'date':
- $attributeData['value'] = $faker->date();
- $attributeData['type'] = $rule;
- break;
- case 'email':
- $attributeData['value'] = $faker->safeEmail;
- $attributeData['type'] = $rule;
- break;
- case 'string':
- $attributeData['value'] = $faker->word;
- $attributeData['type'] = $rule;
- break;
- case 'integer':
- $attributeData['value'] = $faker->randomNumber();
- $attributeData['type'] = $rule;
- break;
- case 'numeric':
- $attributeData['value'] = $faker->randomNumber();
- $attributeData['type'] = $rule;
- break;
- case 'url':
- $attributeData['value'] = $faker->url;
- $attributeData['type'] = $rule;
- break;
- case 'ip':
- $attributeData['value'] = $faker->ipv4;
- $attributeData['type'] = $rule;
- break;
- default:
- $unknownRuleDescription = Description::parse($rule)->getDescription();
- if ($unknownRuleDescription) {
- $attributeData['description'][] = $unknownRuleDescription;
- }
- break;
- }
- if ($attributeData['value'] === '') {
- $attributeData['value'] = $faker->word;
- }
- if (is_null($attributeData['type'])) {
- $attributeData['type'] = 'string';
- }
- }
- /**
- * Call the given URI and return the Response.
- *
- * @param string $method
- * @param string $uri
- * @param array $parameters
- * @param array $cookies
- * @param array $files
- * @param array $server
- * @param string $content
- *
- * @return \Illuminate\Http\Response
- */
- abstract public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null);
- /**
- * Transform headers array to array of $_SERVER vars with HTTP_* format.
- *
- * @param array $headers
- *
- * @return array
- */
- protected function transformHeadersToServerVars(array $headers)
- {
- $server = [];
- $prefix = 'HTTP_';
- foreach ($headers as $name => $value) {
- $name = strtr(strtoupper($name), '-', '_');
- if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') {
- $name = $prefix.$name;
- }
- $server[$name] = $value;
- }
- return $server;
- }
- /**
- * Parse a string based rule.
- *
- * @param string $rules
- *
- * @return array
- */
- protected function parseStringRule($rules)
- {
- $parameters = [];
- // The format for specifying validation rules and parameters follows an
- // easy {rule}:{parameters} formatting convention. For instance the
- // rule "Max:3" states that the value may only be three letters.
- if (strpos($rules, ':') !== false) {
- list($rules, $parameter) = explode(':', $rules, 2);
- $parameters = $this->parseParameters($rules, $parameter);
- }
- return [strtolower(trim($rules)), $parameters];
- }
- /**
- * Parse a parameter list.
- *
- * @param string $rule
- * @param string $parameter
- *
- * @return array
- */
- protected function parseParameters($rule, $parameter)
- {
- if (strtolower($rule) === 'regex') {
- return [$parameter];
- }
- return str_getcsv($parameter);
- }
- /**
- * Normalizes a rule so that we can accept short types.
- *
- * @param string $rule
- *
- * @return string
- */
- protected function normalizeRule($rule)
- {
- switch ($rule) {
- case 'int':
- return 'integer';
- case 'bool':
- return 'boolean';
- default:
- return $rule;
- }
- }
- /**
- * @param $response
- * @return mixed
- */
- private function getResponseContent($response)
- {
- if (empty($response)) {
- return '';
- }
- if ($response->headers->get('Content-Type') === 'application/json') {
- $content = json_decode($response->getContent(), JSON_PRETTY_PRINT);
- } else {
- $content = $response->getContent();
- }
- return $content;
- }
- }
|