├── .gitignore ├── Builder.php ├── Config.php ├── Exception.php ├── Extractor.php ├── LICENSE ├── README.md ├── Resources └── views │ └── default │ ├── index.html │ └── partial │ ├── main.html │ ├── paramContentTpl.html │ ├── paramSampleBtnTpl.html │ ├── paramTableTpl.html │ ├── samplePostBodyTpl.html │ ├── sampleReponseHeaderTpl.html │ ├── sampleReponseTpl.html │ ├── sandboxFormInputTpl.html │ └── sandboxFormTpl.html ├── Response.php ├── Template.php ├── TestClasses ├── Article.php └── User.php ├── View.php ├── View ├── BaseView.php ├── JsonView.php └── ViewInterface.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .project 3 | .settings 4 | -------------------------------------------------------------------------------- /Builder.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Builder 17 | { 18 | /** 19 | * Version number 20 | * 21 | * @var string 22 | */ 23 | const VERSION = '1.3.8'; 24 | 25 | /** 26 | * Classes collection 27 | * 28 | * @var array 29 | */ 30 | protected $_st_classes; 31 | 32 | /** 33 | * Output directory for documentation 34 | * 35 | * @var string 36 | */ 37 | protected $_output_dir; 38 | 39 | /** 40 | * Title to be displayed 41 | * @var string 42 | */ 43 | protected $_title; 44 | 45 | /** 46 | * Output filename for documentation 47 | * 48 | * @var string 49 | */ 50 | protected $_output_file; 51 | 52 | /** 53 | * Template file path 54 | * @var string 55 | **/ 56 | protected $template_path = __DIR__.'/Resources/views/default'; 57 | 58 | /** 59 | * Template object 60 | * @var Template 61 | */ 62 | protected $o_template; 63 | 64 | /** 65 | * Constructor 66 | * 67 | * @param array $st_classes 68 | */ 69 | public function __construct(array $st_classes, $s_output_dir, $title = 'php-apidoc', $s_output_file = 'index.html', $template_path = null) 70 | { 71 | $this->_st_classes = $st_classes; 72 | $this->_output_dir = $s_output_dir; 73 | $this->_title = $title; 74 | $this->_output_file = $s_output_file; 75 | 76 | if ($template_path) { 77 | $this->template_path = $template_path; 78 | } 79 | 80 | $this->o_template = new Template(); 81 | } 82 | 83 | /** 84 | * Extract annotations 85 | * 86 | * @return array 87 | */ 88 | protected function extractAnnotations() 89 | { 90 | foreach ($this->_st_classes as $class) { 91 | $st_output[] = Extractor::getAllClassAnnotations($class); 92 | } 93 | 94 | return end($st_output); 95 | } 96 | 97 | protected function saveTemplate($data, $file) 98 | { 99 | $this->o_template->assign('content', $data); 100 | $this->o_template->assign('title', $this->_title); 101 | $this->o_template->assign('date', date('Y-m-d, H:i:s')); 102 | $this->o_template->assign('version', static::VERSION); 103 | 104 | $newContent = $this->o_template->parse($this->template_path.'/index.html'); 105 | 106 | if (!is_dir($this->_output_dir)) { 107 | if (!mkdir($this->_output_dir)) { 108 | throw new Exception('Cannot create directory'); 109 | } 110 | } 111 | if (!file_put_contents($this->_output_dir.'/'.$file, $newContent)) { 112 | throw new Exception('Cannot save the content to '.$this->_output_dir); 113 | } 114 | } 115 | 116 | /** 117 | * Generate the content of the documentation 118 | * 119 | * @return boolean 120 | */ 121 | protected function generateTemplate() 122 | { 123 | $st_annotations = $this->extractAnnotations(); 124 | 125 | $template = array(); 126 | $counter = 0; 127 | $section = null; 128 | $partial_template = $this->loadPartialTemplate('main'); 129 | 130 | foreach ($st_annotations as $class => $methods) { 131 | foreach ($methods as $name => $docs) { 132 | if (isset($docs['ApiDescription'][0]['section'])) { 133 | $section = $docs['ApiDescription'][0]['section']; 134 | }elseif(isset($docs['ApiSector'][0]['name'])){ 135 | $section = $docs['ApiSector'][0]['name']; 136 | }else{ 137 | $section = $class; 138 | } 139 | if (0 === count($docs)) { 140 | continue; 141 | } 142 | 143 | $sampleOutput = $this->generateSampleOutput($docs, $counter); 144 | 145 | $tr = array( 146 | '{{ elt_id }}' => $counter, 147 | '{{ method }}' => $this->generateBadgeForMethod($docs), 148 | '{{ route }}' => $docs['ApiRoute'][0]['name'], 149 | '{{ description }}' => $docs['ApiDescription'][0]['description'], 150 | '{{ headers }}' => $this->generateHeadersTemplate($counter, $docs), 151 | '{{ parameters }}' => $this->generateParamsTemplate($counter, $docs), 152 | '{{ body }}' => $this->generateBodyTemplate($counter, $docs), 153 | '{{ sandbox_form }}' => $this->generateSandboxForm($docs, $counter), 154 | '{{ sample_response_headers }}' => $sampleOutput[0], 155 | '{{ sample_response_body }}' => $sampleOutput[1] 156 | ); 157 | 158 | $template[$section][] = strtr($partial_template, $tr); 159 | $counter++; 160 | } 161 | } 162 | 163 | $output = ''; 164 | 165 | foreach ($template as $key => $value) { 166 | array_unshift($value, '

' . $key . '

'); 167 | $output .= implode(PHP_EOL, $value); 168 | } 169 | 170 | $this->saveTemplate($output, $this->_output_file); 171 | 172 | return true; 173 | } 174 | 175 | /** 176 | * Generate the sample output 177 | * 178 | * @param array $st_params 179 | * @param integer $counter 180 | * @return string 181 | */ 182 | protected function generateSampleOutput($st_params, $counter) 183 | { 184 | if (!isset($st_params['ApiReturn'])) { 185 | $responseBody = ''; 186 | } else { 187 | $ret = []; 188 | $partial_template = $this->loadPartialTemplate('sampleReponseTpl'); 189 | foreach ($st_params['ApiReturn'] as $params) { 190 | if (in_array($params['type'], array('object', 'array(object) ', 'array', 'string', 'boolean', 'integer', 'number')) && isset($params['sample'])) { 191 | $tr = array( 192 | '{{ elt_id }}' => $counter, 193 | '{{ response }}' => $params['sample'], 194 | '{{ description }}' => '', 195 | ); 196 | if (isset($params['description'])) { 197 | $tr['{{ description }}'] = $params['description']; 198 | } 199 | $ret[] = strtr($partial_template, $tr); 200 | } 201 | } 202 | 203 | $responseBody = implode(PHP_EOL, $ret); 204 | } 205 | 206 | if(!isset($st_params['ApiReturnHeaders'])) { 207 | $responseHeaders = ''; 208 | } else { 209 | $ret = []; 210 | $partial_template = $this->loadPartialTemplate('sampleReponseHeaderTpl'); 211 | 212 | foreach ($st_params['ApiReturnHeaders'] as $headers) { 213 | if(isset($headers['sample'])) { 214 | $tr = array( 215 | '{{ elt_id }}' => $counter, 216 | '{{ response }}' => $headers['sample'], 217 | '{{ description }}' => '' 218 | ); 219 | 220 | $ret[] = strtr($partial_template, $tr); 221 | } 222 | } 223 | 224 | $responseHeaders = implode(PHP_EOL, $ret); 225 | } 226 | 227 | return array($responseHeaders, $responseBody); 228 | } 229 | 230 | /** 231 | * Generates the template for headers 232 | * @param int $id 233 | * @param array $st_params 234 | * @return void|string 235 | */ 236 | protected function generateHeadersTemplate($id, $st_params) 237 | { 238 | if (!isset($st_params['ApiHeaders'])) 239 | { 240 | return; 241 | } 242 | 243 | $body = []; 244 | $partial_template = $this->loadPartialTemplate('paramContentTpl'); 245 | 246 | foreach ($st_params['ApiHeaders'] as $params) { 247 | $tr = array( 248 | '{{ name }}' => $params['name'], 249 | '{{ type }}' => $params['type'], 250 | '{{ nullable }}' => @$params['nullable'] == '1' ? 'No' : 'Yes', 251 | '{{ description }}' => @$params['description'], 252 | ); 253 | $body[] = strtr($partial_template, $tr); 254 | } 255 | 256 | return strtr($this->loadPartialTemplate('paramTableTpl'), array('{{ tbody }}' => implode(PHP_EOL, $body))); 257 | 258 | } 259 | 260 | /** 261 | * Generates the template for parameters 262 | * 263 | * @param int $id 264 | * @param array $st_params 265 | * @return void|string 266 | */ 267 | protected function generateParamsTemplate($id, $st_params) 268 | { 269 | if (!isset($st_params['ApiParams'])) 270 | { 271 | return; 272 | } 273 | 274 | $body = []; 275 | $paramSampleBtnTpl = $this->loadPartialTemplate('paramSampleBtnTpl'); 276 | $paramContentTpl = $this->loadPartialTemplate('paramContentTpl'); 277 | 278 | foreach ($st_params['ApiParams'] as $params) { 279 | $tr = array( 280 | '{{ name }}' => $params['name'], 281 | '{{ type }}' => $params['type'], 282 | '{{ nullable }}' => @$params['nullable'] == '1' ? 'No' : 'Yes', 283 | '{{ description }}' => @$params['description'], 284 | ); 285 | 286 | if (isset($params['sample'])) { 287 | $tr['{{ type }}'].= ' '.strtr($paramSampleBtnTpl, array('{{ sample }}' => $params['sample'])); 288 | } 289 | 290 | $body[] = strtr($paramContentTpl, $tr); 291 | } 292 | 293 | return strtr($this->loadPartialTemplate('paramTableTpl'), array('{{ tbody }}' => implode(PHP_EOL, $body))); 294 | } 295 | 296 | /** 297 | * Generate POST body template 298 | * 299 | * @param int $id 300 | * @param array $body 301 | * @return void|string 302 | */ 303 | private function generateBodyTemplate($id, $docs) 304 | { 305 | if (!isset($docs['ApiBody'])) 306 | { 307 | return; 308 | } 309 | 310 | $body = $docs['ApiBody'][0]; 311 | 312 | return strtr($this->loadPartialTemplate('samplePostBodyTpl'), array( 313 | '{{ elt_id }}' => $id, 314 | '{{ body }}' => $body['sample'] 315 | )); 316 | 317 | } 318 | 319 | /** 320 | * Generate route paramteres form 321 | * 322 | * @param array $st_params 323 | * @param integer $counter 324 | * @return void|mixed 325 | */ 326 | protected function generateSandboxForm($st_params, $counter) 327 | { 328 | $headers = []; 329 | $params = []; 330 | 331 | $sandboxFormInputTpl = $this->loadPartialTemplate('sandboxFormInputTpl'); 332 | 333 | if (isset($st_params['ApiParams']) && is_array($st_params['ApiParams'])) 334 | { 335 | foreach ($st_params['ApiParams'] as $param) 336 | { 337 | $params[] = strtr($sandboxFormInputTpl, [ 338 | '{{ type }}' => $param['type'], 339 | '{{ name }}' => $param['name'], 340 | '{{ description }}' => $param['description'], 341 | '{{ sample }}' => $param['sample'] 342 | ]); 343 | } 344 | } 345 | 346 | if (isset($st_params['ApiHeaders']) && is_array($st_params['ApiHeaders'])) 347 | { 348 | foreach ($st_params['ApiHeaders'] as $header) 349 | { 350 | $headers[] = strtr($sandboxFormInputTpl, [ 351 | '{{ type }}' => 'text', 352 | '{{ name }}' => $header['name'], 353 | '{{ description }}' => $header['description'], 354 | '{{ sample }}' => $header['sample'] 355 | ]); 356 | } 357 | } 358 | 359 | $tr = array( 360 | '{{ elt_id }}' => $counter, 361 | '{{ method }}' => $st_params['ApiMethod'][0]['type'], 362 | '{{ route }}' => $st_params['ApiRoute'][0]['name'], 363 | '{{ headers }}' => implode(PHP_EOL, $headers), 364 | '{{ params }}' => implode(PHP_EOL, $params), 365 | ); 366 | 367 | return strtr($this->loadPartialTemplate('sandboxFormTpl'), $tr); 368 | } 369 | 370 | /** 371 | * Generates a badge for method 372 | * 373 | * @param array $data 374 | * @return string 375 | */ 376 | protected function generateBadgeForMethod($data) 377 | { 378 | $method = strtoupper($data['ApiMethod'][0]['type']); 379 | 380 | $st_labels = [ 381 | 'POST' => 'label-primary', 382 | 'GET' => 'label-success', 383 | 'PUT' => 'label-warning', 384 | 'DELETE' => 'label-danger', 385 | 'PATCH' => 'label-default', 386 | 'OPTIONS'=> 'label-info' 387 | ]; 388 | 389 | return ''.$method.''; 390 | } 391 | 392 | /** 393 | * Load required template 394 | * 395 | * @param string $s_name Template name 396 | * @return string 397 | */ 398 | public function loadPartialTemplate($s_name) 399 | { 400 | $content = file_get_contents($this->template_path."/partial/".$s_name.".html"); 401 | 402 | return $content; 403 | } 404 | 405 | /** 406 | * Output the annotations in json format 407 | * 408 | * @return json 409 | */ 410 | public function renderJson() 411 | { 412 | $st_annotations = $this->extractAnnotations(); 413 | 414 | $o_view = new JsonView(); 415 | $o_view->set('annotations', $st_annotations); 416 | $o_view->render(); 417 | } 418 | 419 | /** 420 | * Output the annotations in json format 421 | * 422 | * @return array 423 | */ 424 | public function renderArray() 425 | { 426 | return $this->extractAnnotations(); 427 | } 428 | 429 | /** 430 | * Build the docs 431 | */ 432 | public function generate() 433 | { 434 | return $this->generateTemplate(); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /Config.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Config 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Exception.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Exception extends \Exception 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Extractor.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Extractor 17 | { 18 | /** 19 | * Static array to store already parsed annotations 20 | * @var array 21 | */ 22 | private static $annotationCache; 23 | 24 | /** 25 | * Indicates that annotations should has strict behavior, 'false' by default 26 | * @var boolean 27 | */ 28 | private $strict = false; 29 | 30 | /** 31 | * Stores the default namespace for Objects instance, usually used on methods like getMethodAnnotationsObjects() 32 | * @var string 33 | */ 34 | public $defaultNamespace = ''; 35 | 36 | /** 37 | * Sets strict variable to true/false 38 | * @param bool $value boolean value to indicate that annotations to has strict behavior 39 | */ 40 | public function setStrict($value) 41 | { 42 | $this->strict = (bool) $value; 43 | } 44 | 45 | /** 46 | * Sets default namespace to use in object instantiation 47 | * @param string $namespace default namespace 48 | */ 49 | public function setDefaultNamespace($namespace) 50 | { 51 | $this->defaultNamespace = $namespace; 52 | } 53 | 54 | /** 55 | * Gets default namespace used in object instantiation 56 | * @return string $namespace default namespace 57 | */ 58 | public function getDefaultAnnotationNamespace() 59 | { 60 | return $this->defaultNamespace; 61 | } 62 | 63 | /** 64 | * Gets all anotations with pattern @SomeAnnotation() from a given class 65 | * 66 | * @param string $className class name to get annotations 67 | * @return array self::$annotationCache all annotated elements 68 | */ 69 | public static function getClassAnnotations($className) 70 | { 71 | if (!isset(self::$annotationCache[$className])) { 72 | $class = new \ReflectionClass($className); 73 | self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment()); 74 | } 75 | 76 | return self::$annotationCache[$className]; 77 | } 78 | 79 | public static function getAllClassAnnotations($className) 80 | { 81 | $class = new \ReflectionClass($className); 82 | 83 | foreach ($class->getMethods() as $object) { 84 | self::$annotationCache['annotations'][$className][$object->name] = self::getMethodAnnotations($className, $object->name); 85 | } 86 | 87 | return self::$annotationCache['annotations']; 88 | } 89 | 90 | /** 91 | * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class 92 | * 93 | * @param string $className class name 94 | * @param string $methodName method name to get annotations 95 | * @return array self::$annotationCache all annotated elements of a method given 96 | */ 97 | public static function getMethodAnnotations($className, $methodName) 98 | { 99 | if (!isset(self::$annotationCache[$className . '::' . $methodName])) { 100 | try { 101 | $method = new \ReflectionMethod($className, $methodName); 102 | $class = new \ReflectionClass($className); 103 | $annotations = self::consolidateAnnotations($method->getDocComment(), $class->getDocComment()); 104 | } catch (\ReflectionException $e) { 105 | $annotations = array(); 106 | } 107 | 108 | self::$annotationCache[$className . '::' . $methodName] = $annotations; 109 | } 110 | 111 | return self::$annotationCache[$className . '::' . $methodName]; 112 | } 113 | 114 | /** 115 | * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class 116 | * and instance its abcAnnotation class 117 | * 118 | * @param string $className class name 119 | * @param string $methodName method name to get annotations 120 | * @return array self::$annotationCache all annotated objects of a method given 121 | */ 122 | public function getMethodAnnotationsObjects($className, $methodName) 123 | { 124 | $annotations = $this->getMethodAnnotations($className, $methodName); 125 | $objects = array(); 126 | 127 | $i = 0; 128 | 129 | foreach ($annotations as $annotationClass => $listParams) { 130 | $annotationClass = ucfirst($annotationClass); 131 | $class = $this->defaultNamespace . $annotationClass . 'Annotation'; 132 | 133 | // verify is the annotation class exists, depending if Annotations::strict is true 134 | // if not, just skip the annotation instance creation. 135 | if (! class_exists($class)) { 136 | if ($this->strict) { 137 | throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class)); 138 | } else { 139 | // silent skip & continue 140 | continue; 141 | } 142 | } 143 | 144 | if (empty($objects[$annotationClass])) { 145 | $objects[$annotationClass] = new $class(); 146 | } 147 | 148 | foreach ($listParams as $params) { 149 | if (is_array($params)) { 150 | foreach ($params as $key => $value) { 151 | $objects[$annotationClass]->set($key, $value); 152 | } 153 | } else { 154 | $objects[$annotationClass]->set($i++, $params); 155 | } 156 | } 157 | } 158 | 159 | return $objects; 160 | } 161 | 162 | private static function consolidateAnnotations ($docblockMethod, $dockblockClass) 163 | { 164 | $methodAnnotations = self::parseAnnotations($docblockMethod); 165 | $classAnnotations = self::parseAnnotations($dockblockClass); 166 | 167 | if(count($methodAnnotations) === 0) { 168 | return array(); 169 | } 170 | 171 | foreach ($classAnnotations as $name => $valueClass) { 172 | if (count($valueClass) !== 1) { 173 | continue; 174 | } 175 | 176 | if ($name === 'ApiRoute') { 177 | if (isset($methodAnnotations[$name])) { 178 | foreach ($methodAnnotations[$name] as $key => $valueMethod) { 179 | $methodAnnotations[$name][$key]['name'] = $valueClass[0]['name'] . $valueMethod['name']; 180 | } 181 | } 182 | } 183 | 184 | if($name === 'ApiSector') { 185 | $methodAnnotations[$name] = $valueClass; 186 | } 187 | } 188 | 189 | return $methodAnnotations; 190 | } 191 | 192 | 193 | 194 | /** 195 | * Parse annotations 196 | * 197 | * @param string $docblock 198 | * @return array parsed annotations params 199 | */ 200 | private static function parseAnnotations($docblock) 201 | { 202 | $annotations = array(); 203 | 204 | // Strip away the docblock header and footer to ease parsing of one line annotations 205 | $docblock = substr($docblock, 3, -2); 206 | 207 | if (preg_match_all('/@(?[A-Za-z_-]+)[\s\t]*\((?(?:(?!\)).)*)\)\r?/s', $docblock, $matches)) { 208 | $numMatches = count($matches[0]); 209 | 210 | for ($i = 0; $i < $numMatches; ++$i) { 211 | // annotations has arguments 212 | if (isset($matches['args'][$i])) { 213 | $argsParts = trim($matches['args'][$i]); 214 | $name = $matches['name'][$i]; 215 | $value = self::parseArgs($argsParts); 216 | } else { 217 | $value = array(); 218 | } 219 | 220 | $annotations[$name][] = $value; 221 | } 222 | } 223 | 224 | return $annotations; 225 | } 226 | 227 | /** 228 | * Parse individual annotation arguments 229 | * 230 | * @param string $content arguments string 231 | * @return array annotated arguments 232 | */ 233 | private static function parseArgs($content) 234 | { 235 | // Replace initial stars 236 | $content = preg_replace('/^\s*\*/m', '', $content); 237 | 238 | $data = array(); 239 | $len = strlen($content); 240 | $i = 0; 241 | $var = ''; 242 | $val = ''; 243 | $level = 1; 244 | 245 | $prevDelimiter = ''; 246 | $nextDelimiter = ''; 247 | $nextToken = ''; 248 | $composing = false; 249 | $type = 'plain'; 250 | $delimiter = null; 251 | $quoted = false; 252 | $tokens = array('"', '"', '{', '}', ',', '='); 253 | 254 | while ($i <= $len) { 255 | $prev_c = substr($content, $i -1, 1); 256 | $c = substr($content, $i++, 1); 257 | 258 | if ($c === '"' && $prev_c !== "\\") { 259 | $delimiter = $c; 260 | //open delimiter 261 | if (!$composing && empty($prevDelimiter) && empty($nextDelimiter)) { 262 | $prevDelimiter = $nextDelimiter = $delimiter; 263 | $val = ''; 264 | $composing = true; 265 | $quoted = true; 266 | } else { 267 | // close delimiter 268 | if ($c !== $nextDelimiter) { 269 | throw new Exception(sprintf( 270 | "Parse Error: enclosing error -> expected: [%s], given: [%s]", 271 | $nextDelimiter, $c 272 | )); 273 | } 274 | 275 | // validating syntax 276 | if ($i < $len) { 277 | if (',' !== substr($content, $i, 1) && '\\' !== $prev_c) { 278 | throw new Exception(sprintf( 279 | "Parse Error: missing comma separator near: ...%s<--", 280 | substr($content, ($i-10), $i) 281 | )); 282 | } 283 | } 284 | 285 | $prevDelimiter = $nextDelimiter = ''; 286 | $composing = false; 287 | $delimiter = null; 288 | } 289 | } elseif (!$composing && in_array($c, $tokens)) { 290 | switch ($c) { 291 | case '=': 292 | $prevDelimiter = $nextDelimiter = ''; 293 | $level = 2; 294 | $composing = false; 295 | $type = 'assoc'; 296 | $quoted = false; 297 | break; 298 | case ',': 299 | $level = 3; 300 | 301 | // If composing flag is true yet, 302 | // it means that the string was not enclosed, so it is parsing error. 303 | if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter)) { 304 | throw new Exception(sprintf( 305 | "Parse Error: enclosing error -> expected: [%s], given: [%s]", 306 | $nextDelimiter, $c 307 | )); 308 | } 309 | 310 | $prevDelimiter = $nextDelimiter = ''; 311 | break; 312 | case '{': 313 | $subc = ''; 314 | $subComposing = true; 315 | 316 | while ($i <= $len) { 317 | $c = substr($content, $i++, 1); 318 | 319 | if (isset($delimiter) && $c === $delimiter) { 320 | throw new Exception(sprintf( 321 | "Parse Error: Composite variable is not enclosed correctly." 322 | )); 323 | } 324 | 325 | if ($c === '}') { 326 | $subComposing = false; 327 | break; 328 | } 329 | $subc .= $c; 330 | } 331 | 332 | // if the string is composing yet means that the structure of var. never was enclosed with '}' 333 | if ($subComposing) { 334 | throw new Exception(sprintf( 335 | "Parse Error: Composite variable is not enclosed correctly. near: ...%s'", 336 | $subc 337 | )); 338 | } 339 | 340 | $val = self::parseArgs($subc); 341 | break; 342 | } 343 | } else { 344 | if ($level == 1) { 345 | $var .= $c; 346 | } elseif ($level == 2) { 347 | $val .= $c; 348 | } 349 | } 350 | 351 | if ($level === 3 || $i === $len) { 352 | if ($type == 'plain' && $i === $len) { 353 | $data = self::castValue($var); 354 | } else { 355 | $data[trim($var)] = self::castValue($val, !$quoted); 356 | } 357 | 358 | $level = 1; 359 | $var = $val = ''; 360 | $composing = false; 361 | $quoted = false; 362 | } 363 | } 364 | 365 | return $data; 366 | } 367 | 368 | /** 369 | * Try determinate the original type variable of a string 370 | * 371 | * @param string $val string containing possibles variables that can be cast to bool or int 372 | * @param boolean $trim indicate if the value passed should be trimmed after to try cast 373 | * @return mixed returns the value converted to original type if was possible 374 | */ 375 | private static function castValue($val, $trim = false) 376 | { 377 | if (is_array($val)) { 378 | foreach ($val as $key => $value) { 379 | $val[$key] = self::castValue($value); 380 | } 381 | } elseif (is_string($val)) { 382 | if ($trim) { 383 | $val = trim($val); 384 | } 385 | $val = stripslashes($val); 386 | $tmp = strtolower($val); 387 | 388 | if ($tmp === 'false' || $tmp === 'true') { 389 | $val = $tmp === 'true'; 390 | } elseif (is_numeric($val)) { 391 | return $val + 0; 392 | } 393 | 394 | unset($tmp); 395 | } 396 | 397 | return $val; 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Calin Rada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-apidoc 2 | ========== 3 | 4 | Generate documentation for php API based application. No dependency. No framework required. 5 | 6 | * [Requirements](#requirements) 7 | * [Installation](#installation) 8 | * [Usage](#usage) 9 | * [Available Methods](#methods) 10 | * [Preview](#preview) 11 | * [Tips](#tips) 12 | * [Known issues](#known-issues) 13 | * [TODO](#todo) 14 | 15 | ### Requirements 16 | 17 | PHP >= 5.3.2 18 | 19 | ### Installation 20 | 21 | The recommended installation is via composer. Just add the following line to your composer.json: 22 | 23 | ```json 24 | { 25 | ... 26 | "require": { 27 | ... 28 | "crada/php-apidoc": "@dev" 29 | } 30 | } 31 | ``` 32 | 33 | ```bash 34 | $ php composer.phar update 35 | ``` 36 | ### Usage 37 | 38 | ```php 39 | generate(); 99 | } catch (Exception $e) { 100 | echo 'There was an error generating the documentation: ', $e->getMessage(); 101 | } 102 | 103 | ``` 104 | 105 | Then, execute it via CLI 106 | 107 | ```php 108 | $ php apidoc.php 109 | ``` 110 | 111 | ### Available Methods 112 | 113 | Here is the list of methods available so far : 114 | 115 | * @ApiDescription(section="...", description="...") 116 | * @ApiMethod(type="(get|post|put|delete|patch") 117 | * @ApiRoute(name="...") 118 | * @ApiParams(name="...", type="...", nullable=..., description="...", [sample=".."]) 119 | * @ApiHeaders(name="...", type="...", nullable=..., description="...") 120 | * @ApiReturnHeaders(sample="...") 121 | * @ApiReturn(type="...", sample="...") 122 | * @ApiBody(sample="...") 123 | 124 | ### Preview 125 | 126 | You can see a dummy generated documentation on http://calinrada.github.io/php-apidoc/ 127 | 128 | ### Tips 129 | 130 | To generate complex object sample input, use the ApiParam "type=(object|array(object)|array)": 131 | 132 | ```php 133 | * @ApiParams(name="data", type="object", sample="{'user_id':'int','profile':{'email':'string','age':'integer'}}") 134 | ``` 135 | 136 | ### Known issues 137 | 138 | I don't know any, but please tell me if you find something. PS: I have tested it only in Chrome ! 139 | 140 | ### TODO 141 | 142 | * Implement options for JSONP 143 | * Implement "add fields" option 144 | 145 | -------------------------------------------------------------------------------- /Resources/views/default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{title}} 10 | 11 | 24 | 25 | 26 | 27 | 67 | 68 |
69 |
70 |
71 |
Api URL
72 | 73 |
74 |
75 |
76 |
77 | {{content}} 78 |
79 |
80 | 81 | 89 | 90 |
91 | 92 | 93 | 94 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /Resources/views/default/partial/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ method }} {{ route }} 5 |

6 |
7 |
8 |
9 | 10 | 11 | 16 | 17 | 18 |
19 | 20 |
21 |
22 | {{ description }} 23 |
24 |
25 |
Headers
26 |
27 | {{ headers }} 28 |
29 |
30 |
31 |
Parameters
32 |
33 | {{ parameters }} 34 |
35 |
36 |
37 |
Body
38 |
39 | {{ body }} 40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | {{ sandbox_form }} 48 |
49 |
50 | Response 51 |
52 |
53 |

54 |                                 

55 |                             
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 | {{ sample_response_headers }} 64 | {{ sample_response_body }} 65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /Resources/views/default/partial/paramContentTpl.html: -------------------------------------------------------------------------------- 1 | 2 | {{ name }} 3 | {{ type }} 4 | {{ nullable }} 5 | {{ description }} 6 | 7 | -------------------------------------------------------------------------------- /Resources/views/default/partial/paramSampleBtnTpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Resources/views/default/partial/paramTableTpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ tbody }} 12 | 13 |
NameTypeRequiredDescription
14 | -------------------------------------------------------------------------------- /Resources/views/default/partial/samplePostBodyTpl.html: -------------------------------------------------------------------------------- 1 |
{{ body }}
2 | -------------------------------------------------------------------------------- /Resources/views/default/partial/sampleReponseHeaderTpl.html: -------------------------------------------------------------------------------- 1 |
{{ response }}
2 | -------------------------------------------------------------------------------- /Resources/views/default/partial/sampleReponseTpl.html: -------------------------------------------------------------------------------- 1 | {{ description }} 2 |
3 |
{{ response }}
4 | -------------------------------------------------------------------------------- /Resources/views/default/partial/sandboxFormInputTpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /Resources/views/default/partial/sandboxFormTpl.html: -------------------------------------------------------------------------------- 1 |
2 | Headers 3 |
4 |
5 | {{ headers }} 6 |
7 |
8 |
9 |
10 | 11 | Parameters 12 |
13 | {{ params }} 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /Response.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Response 12 | { 13 | /** 14 | * Set header 15 | * 16 | * @param string $key 17 | * @param string $value 18 | * @param integer $http_response_code Optional 19 | * @example $response->setHeader('Content-type','application/json') 20 | */ 21 | public function setHeader($key, $value, $http_response_code = null) 22 | { 23 | header($key.': '.$value, null, $http_response_code); 24 | } 25 | 26 | /** 27 | * Set content type for the output 28 | * 29 | * @param string $s_contentType 30 | * @see http://www.freeformatter.com/mime-types-list.html 31 | */ 32 | public function setContentType($s_contentType) 33 | { 34 | header('Content-type: '.$s_contentType); 35 | } 36 | 37 | /** 38 | * Sends connection: close header 39 | */ 40 | public function closeConection() 41 | { 42 | header('Connection: close'); 43 | } 44 | 45 | /** 46 | * Wraper for echo 47 | * 48 | * @param $data 49 | */ 50 | public function send($data) 51 | { 52 | echo $data; 53 | exit(0); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Template.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @license http://opensource.org/licenses/bsd-license.php The BSD License 12 | * @author Calin Rada 13 | */ 14 | class Template { 15 | /** 16 | * Content variables 17 | * @access private 18 | * @var array 19 | */ 20 | private $vars = []; 21 | 22 | /** 23 | * Content delimiters 24 | * @access private 25 | * @var string 26 | */ 27 | private $l_delim = '{{', 28 | $r_delim = '}}'; 29 | 30 | /** 31 | * Set template property in template file 32 | * @access public 33 | * @param string $key property name 34 | * @param string $value property value 35 | */ 36 | public function assign( $key, $value ) 37 | { 38 | $this->vars[$key] = $value; 39 | } 40 | 41 | /** 42 | * Parse template file 43 | * @access public 44 | * @param string $template_file 45 | */ 46 | public function parse( $template_file ) 47 | { 48 | if ( file_exists( $template_file ) ) { 49 | $content = file_get_contents($template_file); 50 | 51 | foreach ( $this->vars as $key => $value ) { 52 | if ( is_array( $value ) ) { 53 | $content = $this->parsePair($key, $value, $content); 54 | } else { 55 | $content = $this->parseSingle($key, (string) $value, $content); 56 | } 57 | } 58 | 59 | return $content; 60 | } else { 61 | exit( '

Template error

' ); 62 | } 63 | } 64 | 65 | /** 66 | * Parsing content for single varliable 67 | * @access private 68 | * @param string $key property name 69 | * @param string $value property value 70 | * @param string $string content to replace 71 | * @param integer $index index of loop item 72 | * @return string replaced content 73 | */ 74 | private function parseSingle( $key, $value, $string, $index = null ) 75 | { 76 | if ( isset( $index ) ) { 77 | $string = str_replace( $this->l_delim . '%index%' . $this->r_delim, $index, $string ); 78 | } 79 | return str_replace( $this->l_delim . $key . $this->r_delim, $value, $string ); 80 | } 81 | 82 | /** 83 | * Parsing content for loop varliable 84 | * @access private 85 | * @param string $variable loop name 86 | * @param string $value loop data 87 | * @param string $string content to replace 88 | * @return string replaced content 89 | */ 90 | private function parsePair( $variable, $data, $string ) 91 | { 92 | $match = $this->matchPair($string, $variable); 93 | if( $match == false ) return $string; 94 | 95 | $str = ''; 96 | foreach ( $data as $k_row => $row ) { 97 | $temp = $match['1']; 98 | foreach( $row as $key => $val ) { 99 | if( !is_array( $val ) ) { 100 | $index = array_search( $k_row, array_keys( $data ) ); 101 | $temp = $this->parseSingle( $key, $val, $temp, $index ); 102 | } else { 103 | $temp = $this->parsePair( $key, $val, $temp ); 104 | } 105 | } 106 | $str .= $temp; 107 | } 108 | 109 | return str_replace( $match['0'], $str, $string ); 110 | } 111 | 112 | /** 113 | * Match loop pair 114 | * @access private 115 | * @param string $string content with loop 116 | * @param string $variable loop name 117 | * @return string matched content 118 | */ 119 | private function matchPair( $string, $variable ) 120 | { 121 | if ( !preg_match("|" . preg_quote($this->l_delim) . 'loop ' . $variable . preg_quote($this->r_delim) . "(.+?)". preg_quote($this->l_delim) . 'end loop' . preg_quote($this->r_delim) . "|s", $string, $match ) ) { 122 | return false; 123 | } 124 | 125 | return $match; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /TestClasses/Article.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class View extends BaseView 14 | { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /View/BaseView.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class BaseView implements ViewInterface 14 | { 15 | /** 16 | * Template name 17 | * 18 | * @var string 19 | */ 20 | protected $s_file; 21 | 22 | /** 23 | * Data array that has to be rendered 24 | * 25 | * @var array 26 | */ 27 | protected $st_data = array(); 28 | 29 | /** 30 | * Array with config parameters for the View 31 | * 32 | * @var array 33 | */ 34 | protected $st_viewConfig; 35 | 36 | /** 37 | * View object 38 | * 39 | * @var object 40 | */ 41 | protected $o_view; 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function __init() {} 47 | 48 | /** 49 | * Set configuration params from the config file. 50 | * 51 | * @param array $st_config 52 | */ 53 | public function setConfig($st_config) 54 | { 55 | $this->st_viewConfig = $st_config; 56 | } 57 | 58 | /** 59 | * Get config param(s) 60 | * 61 | * @param string $s_key 62 | * @return string|array|boolean 63 | */ 64 | public function getConfig($s_key = null) 65 | { 66 | if ($s_key) { 67 | if (isset($this->st_viewConfig[$s_key])) { 68 | return $this->st_viewConfig[$s_key]; 69 | } 70 | 71 | return false; 72 | } 73 | 74 | return $this->st_viewConfig; 75 | } 76 | 77 | /** 78 | * Set template file 79 | * @param string $file Full path to file 80 | */ 81 | public function setTemplate($file) 82 | { 83 | $this->s_file = $file; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function getTemplate() 90 | { 91 | return $this->s_file; 92 | } 93 | 94 | /** 95 | * Set parameters to render 96 | * @param string $key 97 | * @param mixed $value Value - can be string / array 98 | * @param boolean $b_skipKey If true, the value will be assign directly to $this->st_data 99 | */ 100 | public function set($key, $value, $b_skipKey = false) 101 | { 102 | if (true === $b_skipKey) { 103 | $this->st_data = $value; 104 | } else { 105 | $this->st_data[$key] = $value; 106 | } 107 | } 108 | 109 | /** 110 | * Get a value by key 111 | * @param string $key Key name 112 | */ 113 | public function get($key) 114 | { 115 | return $this->st_data[$key]; 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function getAll() 122 | { 123 | return $this->st_data; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function render() 130 | { 131 | $file = str_replace('\\','/',$this->getTemplate()).'.php'; 132 | if (!file_exists($file)) { 133 | throw new Exception('Template ' . $file . ' does not exist.'); 134 | } 135 | extract($this->st_data); 136 | ob_start(); 137 | include($file); 138 | $output = ob_get_contents(); 139 | ob_end_clean(); 140 | echo $output; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /View/JsonView.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class JsonView extends BaseView 12 | { 13 | /** 14 | * {@inheritdoc} 15 | */ 16 | public function render() 17 | { 18 | $data = json_encode($this->st_data, JSON_FORCE_OBJECT); 19 | 20 | $response = new \Crada\Apidoc\Response(); 21 | $response->setContentType('application/json'); 22 | $response->closeConection(); 23 | $response->send($data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /View/ViewInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ViewInterface 14 | { 15 | /** 16 | * Act as constructor 17 | */ 18 | public function __init(); 19 | 20 | /** 21 | * Set template file 22 | * @param string $file Full path to file 23 | */ 24 | public function setTemplate($file); 25 | 26 | /** 27 | * Get current template 28 | */ 29 | public function getTemplate(); 30 | 31 | /** 32 | * Set parameters to render 33 | * @param string $key 34 | * @param mixed $value Value - can be string / array 35 | */ 36 | public function set($key, $value); 37 | 38 | /** 39 | * Get a value by key 40 | * @param string $key Key name 41 | */ 42 | public function get($key); 43 | 44 | /** 45 | * Get all values 46 | */ 47 | public function getAll(); 48 | 49 | /** 50 | * Render template 51 | */ 52 | public function render(); 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crada/php-apidoc", 3 | "type": "library", 4 | "description": "Generate documentation for php API based application. No dependency. No framework required.", 5 | "keywords": ["php","api","documentation","generator"], 6 | "homepage": "https://github.com/calinrada/php-apidoc", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Calin Rada", 11 | "email": "rada.calin@gmail.com" 12 | }, { 13 | "name": "Kevin Saliou", 14 | "email": "kevin@saliou.name" 15 | }, { 16 | "name": "Borja Santos-Diez", 17 | "email": "borja@santosdiez.es" 18 | } 19 | ], 20 | "minimum-stability": "dev", 21 | "require": { 22 | "php": ">=5.3.2" 23 | }, 24 | "autoload": { 25 | "psr-0": { "Crada": "" } 26 | }, 27 | "target-dir": "Crada/Apidoc", 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "1.3-dev" 31 | } 32 | } 33 | } 34 | --------------------------------------------------------------------------------