mirror of
https://github.com/YunoHost-Apps/dolibarr_ynh.git
synced 2024-09-03 18:35:53 +02:00
467 lines
14 KiB
PHP
467 lines
14 KiB
PHP
|
<?php
|
||
|
namespace Luracast\Restler;
|
||
|
|
||
|
use Exception;
|
||
|
|
||
|
/**
|
||
|
* Parses the PHPDoc comments for metadata. Inspired by `Documentor` code base.
|
||
|
*
|
||
|
* @category Framework
|
||
|
* @package Restler
|
||
|
* @subpackage Helper
|
||
|
* @author R.Arul Kumaran <arul@luracast.com>
|
||
|
* @copyright 2010 Luracast
|
||
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
||
|
* @link http://luracast.com/products/restler/
|
||
|
* @version 3.0.0rc5
|
||
|
*/
|
||
|
class CommentParser
|
||
|
{
|
||
|
/**
|
||
|
* name for the embedded data
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
public static $embeddedDataName = 'properties';
|
||
|
/**
|
||
|
* Regular Expression pattern for finding the embedded data and extract
|
||
|
* the inner information. It is used with preg_match.
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
public static $embeddedDataPattern
|
||
|
= '/```(\w*)[\s]*(([^`]*`{0,2}[^`]+)*)```/ms';
|
||
|
/**
|
||
|
* Pattern will have groups for the inner details of embedded data
|
||
|
* this index is used to locate the data portion.
|
||
|
*
|
||
|
* @var int
|
||
|
*/
|
||
|
public static $embeddedDataIndex = 2;
|
||
|
/**
|
||
|
* Delimiter used to split the array data.
|
||
|
*
|
||
|
* When the name portion is of the embedded data is blank auto detection
|
||
|
* will be used and if URLEncodedFormat is detected as the data format
|
||
|
* the character specified will be used as the delimiter to find split
|
||
|
* array data.
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
public static $arrayDelimiter = ',';
|
||
|
|
||
|
/**
|
||
|
* character sequence used to escape \@
|
||
|
*/
|
||
|
const escapedAtChar = '\\@';
|
||
|
|
||
|
/**
|
||
|
* character sequence used to escape end of comment
|
||
|
*/
|
||
|
const escapedCommendEnd = '{@*}';
|
||
|
|
||
|
/**
|
||
|
* Instance of Restler class injected at runtime.
|
||
|
*
|
||
|
* @var Restler
|
||
|
*/
|
||
|
public $restler;
|
||
|
/**
|
||
|
* Comment information is parsed and stored in to this array.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private $_data = array();
|
||
|
|
||
|
/**
|
||
|
* Parse the comment and extract the data.
|
||
|
*
|
||
|
* @static
|
||
|
*
|
||
|
* @param $comment
|
||
|
* @param bool $isPhpDoc
|
||
|
*
|
||
|
* @return array associative array with the extracted values
|
||
|
*/
|
||
|
public static function parse($comment, $isPhpDoc = true)
|
||
|
{
|
||
|
$p = new self();
|
||
|
if (empty($comment)) {
|
||
|
return $p->_data;
|
||
|
}
|
||
|
|
||
|
if ($isPhpDoc) {
|
||
|
$comment = self::removeCommentTags($comment);
|
||
|
}
|
||
|
|
||
|
$p->extractData($comment);
|
||
|
return $p->_data;
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes the comment tags from each line of the comment.
|
||
|
*
|
||
|
* @static
|
||
|
*
|
||
|
* @param string $comment PhpDoc style comment
|
||
|
*
|
||
|
* @return string comments with out the tags
|
||
|
*/
|
||
|
public static function removeCommentTags($comment)
|
||
|
{
|
||
|
$pattern = '/(^\/\*\*)|(^\s*\**[ \/]?)|\s(?=@)|\s\*\//m';
|
||
|
return preg_replace($pattern, '', $comment);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Extracts description and long description, uses other methods to get
|
||
|
* parameters.
|
||
|
*
|
||
|
* @param $comment
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
private function extractData($comment)
|
||
|
{
|
||
|
//to use @ as part of comment we need to
|
||
|
$comment = str_replace(
|
||
|
array(self::escapedCommendEnd, self::escapedAtChar),
|
||
|
array('*/', '@'),
|
||
|
$comment);
|
||
|
|
||
|
$description = array();
|
||
|
$longDescription = array();
|
||
|
$params = array();
|
||
|
|
||
|
$mode = 0; // extract short description;
|
||
|
$comments = preg_split("/(\r?\n)/", $comment);
|
||
|
// remove first blank line;
|
||
|
array_shift($comments);
|
||
|
$addNewline = false;
|
||
|
foreach ($comments as $line) {
|
||
|
$line = trim($line);
|
||
|
$newParam = false;
|
||
|
if (empty ($line)) {
|
||
|
if ($mode == 0) {
|
||
|
$mode++;
|
||
|
} else {
|
||
|
$addNewline = true;
|
||
|
}
|
||
|
continue;
|
||
|
} elseif ($line{0} == '@') {
|
||
|
$mode = 2;
|
||
|
$newParam = true;
|
||
|
}
|
||
|
switch ($mode) {
|
||
|
case 0 :
|
||
|
$description[] = $line;
|
||
|
if (count($description) > 3) {
|
||
|
// if more than 3 lines take only first line
|
||
|
$longDescription = $description;
|
||
|
$description[] = array_shift($longDescription);
|
||
|
$mode = 1;
|
||
|
} elseif (substr($line, -1) == '.') {
|
||
|
$mode = 1;
|
||
|
}
|
||
|
break;
|
||
|
case 1 :
|
||
|
if ($addNewline) {
|
||
|
$line = ' ' . $line;
|
||
|
}
|
||
|
$longDescription[] = $line;
|
||
|
break;
|
||
|
case 2 :
|
||
|
$newParam
|
||
|
? $params[] = $line
|
||
|
: $params[count($params) - 1] .= ' ' . $line;
|
||
|
}
|
||
|
$addNewline = false;
|
||
|
}
|
||
|
$description = implode(' ', $description);
|
||
|
$longDescription = implode(' ', $longDescription);
|
||
|
$description = preg_replace('/\s+/msu', ' ', $description);
|
||
|
$longDescription = preg_replace('/\s+/msu', ' ', $longDescription);
|
||
|
list($description, $d1)
|
||
|
= $this->parseEmbeddedData($description);
|
||
|
list($longDescription, $d2)
|
||
|
= $this->parseEmbeddedData($longDescription);
|
||
|
$this->_data = compact('description', 'longDescription');
|
||
|
$d2 += $d1;
|
||
|
if (!empty($d2)) {
|
||
|
$this->_data[self::$embeddedDataName] = $d2;
|
||
|
}
|
||
|
foreach ($params as $key => $line) {
|
||
|
list(, $param, $value) = preg_split('/\@|\s/', $line, 3)
|
||
|
+ array('', '', '');
|
||
|
list($value, $embedded) = $this->parseEmbeddedData($value);
|
||
|
$value = array_filter(preg_split('/\s+/msu', $value));
|
||
|
$this->parseParam($param, $value, $embedded);
|
||
|
}
|
||
|
return $this->_data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse parameters that begin with (at)
|
||
|
*
|
||
|
* @param $param
|
||
|
* @param array $value
|
||
|
* @param array $embedded
|
||
|
*/
|
||
|
private function parseParam($param, array $value, array $embedded)
|
||
|
{
|
||
|
$data = & $this->_data;
|
||
|
$allowMultiple = false;
|
||
|
switch ($param) {
|
||
|
case 'param' :
|
||
|
$value = $this->formatParam($value);
|
||
|
$allowMultiple = true;
|
||
|
break;
|
||
|
case 'var' :
|
||
|
$value = $this->formatVar($value);
|
||
|
break;
|
||
|
case 'return' :
|
||
|
$value = $this->formatReturn($value);
|
||
|
break;
|
||
|
case 'class' :
|
||
|
$data = & $data[$param];
|
||
|
list ($param, $value) = $this->formatClass($value);
|
||
|
break;
|
||
|
case 'access' :
|
||
|
$value = reset($value);
|
||
|
break;
|
||
|
case 'expires' :
|
||
|
case 'status' :
|
||
|
$value = intval(reset($value));
|
||
|
break;
|
||
|
case 'throws' :
|
||
|
$value = $this->formatThrows($value);
|
||
|
$allowMultiple = true;
|
||
|
break;
|
||
|
case 'author':
|
||
|
$value = $this->formatAuthor($value);
|
||
|
$allowMultiple = true;
|
||
|
break;
|
||
|
case 'header' :
|
||
|
case 'link':
|
||
|
case 'example':
|
||
|
case 'todo':
|
||
|
$allowMultiple = true;
|
||
|
//don't break, continue with code for default:
|
||
|
default :
|
||
|
$value = implode(' ', $value);
|
||
|
}
|
||
|
if (!empty($embedded)) {
|
||
|
if (is_string($value)) {
|
||
|
$value = array('description' => $value);
|
||
|
}
|
||
|
$value[self::$embeddedDataName] = $embedded;
|
||
|
}
|
||
|
if (empty ($data[$param])) {
|
||
|
if ($allowMultiple) {
|
||
|
$data[$param] = array(
|
||
|
$value
|
||
|
);
|
||
|
} else {
|
||
|
$data[$param] = $value;
|
||
|
}
|
||
|
} elseif ($allowMultiple) {
|
||
|
$data[$param][] = $value;
|
||
|
} elseif ($param == 'param') {
|
||
|
$arr = array(
|
||
|
$data[$param],
|
||
|
$value
|
||
|
);
|
||
|
$data[$param] = $arr;
|
||
|
} else {
|
||
|
if (!is_string($value) && isset($value[self::$embeddedDataName])
|
||
|
&& isset($data[$param][self::$embeddedDataName])
|
||
|
) {
|
||
|
$value[self::$embeddedDataName]
|
||
|
+= $data[$param][self::$embeddedDataName];
|
||
|
}
|
||
|
$data[$param] = $value + $data[$param];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses the inline php doc comments and embedded data.
|
||
|
*
|
||
|
* @param $subject
|
||
|
*
|
||
|
* @return array
|
||
|
* @throws Exception
|
||
|
*/
|
||
|
private function parseEmbeddedData($subject)
|
||
|
{
|
||
|
$data = array();
|
||
|
|
||
|
//parse {@pattern } tags specially
|
||
|
while (preg_match('|(?s-m)({@pattern (/.+/[imsxuADSUXJ]*)})|', $subject, $matches)) {
|
||
|
$subject = str_replace($matches[0], '', $subject);
|
||
|
$data['pattern'] = $matches[2];
|
||
|
}
|
||
|
while (preg_match('/{@(\w+)\s?([^}]*)}/ms', $subject, $matches)) {
|
||
|
$subject = str_replace($matches[0], '', $subject);
|
||
|
if ($matches[2] == 'true' || $matches[2] == 'false') {
|
||
|
$matches[2] = $matches[2] == 'true';
|
||
|
} elseif ($matches[2] == '') {
|
||
|
$matches[2] = true;
|
||
|
}
|
||
|
if ($matches[1] == 'pattern') {
|
||
|
throw new Exception('Inline pattern tag should follow {@pattern /REGEX_PATTERN_HERE/} format and can optionally include PCRE modifiers following the ending `/`');
|
||
|
} elseif (false !== strpos($matches[2], static::$arrayDelimiter)) {
|
||
|
$matches[2] = explode(static::$arrayDelimiter, $matches[2]);
|
||
|
}
|
||
|
$data[$matches[1]] = $matches[2];
|
||
|
}
|
||
|
|
||
|
while (preg_match(self::$embeddedDataPattern, $subject, $matches)) {
|
||
|
$subject = str_replace($matches[0], '', $subject);
|
||
|
$str = $matches[self::$embeddedDataIndex];
|
||
|
if (isset ($this->restler)
|
||
|
&& self::$embeddedDataIndex > 1
|
||
|
&& !empty ($matches[1])
|
||
|
) {
|
||
|
$extension = $matches[1];
|
||
|
$formatMap = $this->restler->getFormatMap();
|
||
|
if (isset ($formatMap[$extension])) {
|
||
|
/**
|
||
|
* @var \Luracast\Restler\Format\iFormat
|
||
|
*/
|
||
|
$format = $formatMap[$extension];
|
||
|
$format = new $format();
|
||
|
$data = $format->decode($str);
|
||
|
}
|
||
|
} else { // auto detect
|
||
|
if ($str{0} == '{') {
|
||
|
$d = json_decode($str, true);
|
||
|
if (json_last_error() != JSON_ERROR_NONE) {
|
||
|
throw new Exception('Error parsing embedded JSON data'
|
||
|
. " $str");
|
||
|
}
|
||
|
$data = $d + $data;
|
||
|
} else {
|
||
|
parse_str($str, $d);
|
||
|
//clean up
|
||
|
$d = array_filter($d);
|
||
|
foreach ($d as $key => $val) {
|
||
|
$kt = trim($key);
|
||
|
if ($kt != $key) {
|
||
|
unset($d[$key]);
|
||
|
$key = $kt;
|
||
|
$d[$key] = $val;
|
||
|
}
|
||
|
if (is_string($val)) {
|
||
|
if ($val == 'true' || $val == 'false') {
|
||
|
$d[$key] = $val == 'true' ? true : false;
|
||
|
} else {
|
||
|
$val = explode(self::$arrayDelimiter, $val);
|
||
|
if (count($val) > 1) {
|
||
|
$d[$key] = $val;
|
||
|
} else {
|
||
|
$d[$key] =
|
||
|
preg_replace('/\s+/msu', ' ',
|
||
|
$d[$key]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
$data = $d + $data;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return array($subject, $data);
|
||
|
}
|
||
|
|
||
|
private function formatThrows(array $value)
|
||
|
{
|
||
|
$r = array();
|
||
|
$r['code'] = count($value) && is_numeric($value[0])
|
||
|
? intval(array_shift($value)) : 500;
|
||
|
$reason = implode(' ', $value);
|
||
|
$r['reason'] = empty($reason) ? '' : $reason;
|
||
|
return $r;
|
||
|
}
|
||
|
|
||
|
private function formatClass(array $value)
|
||
|
{
|
||
|
$param = array_shift($value);
|
||
|
|
||
|
if (empty($param)) {
|
||
|
$param = 'Unknown';
|
||
|
}
|
||
|
$value = implode(' ', $value);
|
||
|
return array(
|
||
|
ltrim($param, '\\'),
|
||
|
array('description' => $value)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
private function formatAuthor(array $value)
|
||
|
{
|
||
|
$r = array();
|
||
|
$email = end($value);
|
||
|
if ($email{0} == '<') {
|
||
|
$email = substr($email, 1, -1);
|
||
|
array_pop($value);
|
||
|
$r['email'] = $email;
|
||
|
}
|
||
|
$r['name'] = implode(' ', $value);
|
||
|
return $r;
|
||
|
}
|
||
|
|
||
|
private function formatReturn(array $value)
|
||
|
{
|
||
|
$data = explode('|', array_shift($value));
|
||
|
$r = array(
|
||
|
'type' => count($data) == 1 ? $data[0] : $data
|
||
|
);
|
||
|
$r['description'] = implode(' ', $value);
|
||
|
return $r;
|
||
|
}
|
||
|
|
||
|
private function formatParam(array $value)
|
||
|
{
|
||
|
$r = array();
|
||
|
$data = array_shift($value);
|
||
|
if (empty($data)) {
|
||
|
$r['type'] = 'mixed';
|
||
|
} elseif ($data{0} == '$') {
|
||
|
$r['name'] = substr($data, 1);
|
||
|
$r['type'] = 'mixed';
|
||
|
} else {
|
||
|
$data = explode('|', $data);
|
||
|
$r['type'] = count($data) == 1 ? $data[0] : $data;
|
||
|
|
||
|
$data = array_shift($value);
|
||
|
if (!empty($data) && $data{0} == '$') {
|
||
|
$r['name'] = substr($data, 1);
|
||
|
}
|
||
|
}
|
||
|
if ($value) {
|
||
|
$r['description'] = implode(' ', $value);
|
||
|
}
|
||
|
return $r;
|
||
|
}
|
||
|
|
||
|
private function formatVar(array $value)
|
||
|
{
|
||
|
$r = array();
|
||
|
$data = array_shift($value);
|
||
|
if (empty($data)) {
|
||
|
$r['type'] = 'mixed';
|
||
|
} elseif ($data{0} == '$') {
|
||
|
$r['name'] = substr($data, 1);
|
||
|
$r['type'] = 'mixed';
|
||
|
} else {
|
||
|
$data = explode('|', $data);
|
||
|
$r['type'] = count($data) == 1 ? $data[0] : $data;
|
||
|
}
|
||
|
if ($value) {
|
||
|
$r['description'] = implode(' ', $value);
|
||
|
}
|
||
|
return $r;
|
||
|
}
|
||
|
}
|