fork download
  1. <?php
  2. interface ILexer {
  3. /**
  4.   * return associative array containing tokens for parser
  5.   * @param string $input code to tokenize
  6.   * @return array
  7.   */
  8. public function tokenize($input);
  9. }
  10.  
  11. interface INameValidator {
  12. /**
  13.   * Checks if name is valid. In a case of failure trigger syntax error.
  14.   * @param string $name
  15.   * @return void
  16.   */
  17. public function validate($name);
  18. }
  19.  
  20. interface IParser {
  21. /**
  22.   * Sets array of keywords.
  23.   * @param array $keywords associative array where key is keyword and value respective php instruction.
  24.   * @return void
  25.   */
  26. public function setKeywords(array $keywords);
  27.  
  28. /**
  29.   * Parses code. Checks for syntax errors.
  30.   * If no error occurs returns data for compiler to assemble php class
  31.   * otherwise trigger syntax error.
  32.   * @param string $input code to parse.
  33.   * @return array IParserResult
  34.   */
  35. public function parse($input);
  36. }
  37.  
  38. interface IParseResult {
  39. public function getDefinition();
  40. public function getName();
  41. public function getConstants();
  42. }
  43.  
  44. interface ICompiler {
  45. /**
  46.   * Compiles enum into valid php class
  47.   * @param string $input code to compile.
  48.   * @return string
  49.   */
  50. public function compile($input);
  51. }
  52.  
  53. interface IErrorManager {
  54. /**
  55.   * Triggers fatal error.
  56.   * @param string $message error message
  57.   * @return void
  58.   */
  59. public function ariseFatal($message);
  60.  
  61. /**
  62.   * Triggers warning.
  63.   * @param string $message error message
  64.   * @return void
  65.   */
  66. public function ariseWarning($message);
  67. /**
  68.   * Triggers notice.
  69.   * @param string $message error message
  70.   * @return void
  71.   */
  72. public function ariseNotice($message);
  73. }
  74.  
  75. //I know this lexer sucks.
  76. class Lexer extends CompilerElement implements ILexer {
  77. // Potential hidden dependency
  78. private $specialChars = array(
  79. '{' => 'start_body',
  80. '}' => 'end_body'
  81. );
  82.  
  83. public function tokenize($input) {
  84. if (!is_string($input)) {
  85. $this->getErrorManager()->ariseFatal(get_class($this).'::parse expects parameter 1 to be string. '.gettype($input).' given.');
  86. }
  87.  
  88. $tokens = array();
  89. $token = $this->getEmptyToken();
  90. $tokenMetadata = $this->getDefaultTokenMetadata();
  91. $inputLength = strlen($input);
  92.  
  93. for ($i = 0; $i < $inputLength; $i++) {
  94. $current = $input[$i];
  95.  
  96. //check if special character
  97. if (isset($this->specialChars[$current])) {
  98. $token[$this->specialChars[$current]] = $current;
  99.  
  100. if ($i !== $inputLength - 1)
  101. continue;
  102. }
  103.  
  104. if (isset($token['end_body'])) {
  105. $tokens[] = $token;
  106. $token = $this->getEmptyToken();
  107. $tokenMetadata = $this->getDefaultTokenMetadata();
  108.  
  109. if ($i === $inputLength - 1)
  110. continue;
  111. }
  112.  
  113. $isWhitespace = ctype_space($current);
  114.  
  115. if (isset($token['start_body'])) {
  116. //skip if whitespace
  117. if ($isWhitespace) {
  118. continue;
  119. }
  120.  
  121. if ($current === ',') {
  122. $tokenMetadata['newItem'] = true;
  123. $tokenMetadata['newItemNameResolved'] = false;
  124. continue;
  125. }
  126. if ($current === '=') {
  127. $tokenMetadata['newItemNameResolved'] = true;
  128. continue;
  129. }
  130.  
  131. if ($tokenMetadata['newItem']) {
  132. $token['e_'.$current] = null;
  133. $tokenMetadata['newItem'] = false;
  134. } else {
  135. end($token);
  136. $lastKey = key($token);
  137.  
  138. if (!$tokenMetadata['newItemNameResolved']) {
  139. unset($token[$lastKey]);
  140. $token[$lastKey.$current] = null;
  141. } else {
  142. $token[$lastKey] .= $current;
  143. }
  144. }
  145.  
  146. continue;
  147. }
  148.  
  149. if (!$tokenMetadata['typeResolved']) {
  150. if ($isWhitespace) {
  151. if (strlen($token['type']) === 0) {
  152. continue;
  153. } else {
  154. $tokenMetadata['typeResolved'] = true;
  155. }
  156. } else {
  157. $token['type'] .= $current;
  158. }
  159. } else if (!$tokenMetadata['nameResolved']) {
  160. if ($isWhitespace) {
  161. if (strlen($token['name']) === 0) {
  162. continue;
  163. } else {
  164. $tokenMetadata['nameResolved'] = true;
  165. }
  166. } else {
  167. $token['name'] .= $current;
  168. }
  169. }
  170. }
  171.  
  172. return $tokens;
  173. }
  174.  
  175. private function getEmptyToken() {
  176. return array(
  177. 'type' => '',
  178. 'name' => ''
  179. );
  180. }
  181.  
  182. private function getDefaultTokenMetadata() {
  183. return array(
  184. 'typeResolved' => false,
  185. 'nameResolved' => false,
  186. 'newItem' => true,
  187. 'newItemNameResolved' => false
  188. );
  189. }
  190.  
  191. public function __construct(IErrorManager $errorManager) {
  192. parent::__construct($errorManager);
  193. }
  194. }
  195.  
  196. class NameValidator extends CompilerElement implements INameValidator {
  197. private $allowedChars;
  198.  
  199. public function validate($name) {
  200. if (!is_string($name)) {
  201. $this->getErrorManager()->ariseFatal(get_class($this).'::validate expects parameter 1 to be string. '.gettype($name).' given.');
  202. }
  203.  
  204. $nameLength = strlen($name);
  205.  
  206. for ($i = 0; $i < $nameLength; $i++) {
  207. $current = $name[$i];
  208.  
  209. if ($i === 0 && !ctype_alpha($current)) {
  210. $this->getErrorManager()->ariseFatal('Name should start with alphabetic characters. Given name '.$name.' starts with: '.$current);
  211. }
  212. if (!in_array(strtolower($current), $this->allowedChars, true)) {
  213. $this->getErrorManager()->ariseFatal('Unexpected character '.$current.' in name '.$name.'.');
  214. }
  215. }
  216. }
  217.  
  218. public function __construct(IErrorManager $errorManager) {
  219. parent::__construct($errorManager);
  220.  
  221. // Yes, I hate that everything is allowed as name in php.
  222. $this->allowedChars = str_split('abcdefghijklmnopqrstuvwxyz0123456789');
  223. }
  224. }
  225.  
  226. class Parser extends CompilerElement implements IParser {
  227. private $lexer;
  228. private $nameValidator;
  229. private $keywords = array();
  230.  
  231. public function setKeywords(array $keywords) {
  232. $this->keywords = $keywords;
  233. }
  234.  
  235. public function parse($input) {
  236. if (!is_string($input)) {
  237. $this->getErrorManager()->ariseFatal(get_class($this).'::parse expects parameter 1 to be string. '.gettype($input).' given.');
  238. }
  239.  
  240. $input = $this->cleanUp($input);
  241. $errorManager = $this->getErrorManager();
  242. $allTokens = $this->lexer->tokenize($input);
  243.  
  244. $result = array();
  245. $currentResult = null;
  246. $processingBody = false;
  247.  
  248. foreach ($allTokens as $tokens) {
  249. foreach ($tokens as $token => $tokenValue) {
  250. switch ($token) {
  251. case 'type':
  252. if ($currentResult !== null || !isset($this->keywords[$tokenValue])) {
  253. $errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
  254. }
  255.  
  256. $currentResult = new ParseResult($this->keywords[$tokenValue], $errorManager);
  257. break;
  258. case 'name':
  259. if ($currentResult === null) {
  260. $errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
  261. }
  262.  
  263. $this->nameValidator->validate($tokenValue);
  264.  
  265. $currentResult->setName($tokenValue);
  266. break;
  267. case 'start_body':
  268. if ($currentResult === null || $currentResult->getName() === null) {
  269. $errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
  270. }
  271.  
  272. $processingBody = true;
  273. break;
  274. case 'end_body':
  275. if ($currentResult === null || $currentResult->getName() === null || $processingBody === false) {
  276. $errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
  277. }
  278.  
  279. $result[] = $currentResult;
  280. $currentResult = null;
  281. $processingBody = false;
  282. break;
  283. default:
  284. if ($processingBody === true && $currentResult !== null) {
  285. $name = ltrim($token, 'e_');
  286. $this->nameValidator->validate($name);
  287.  
  288. if ($tokenValue === null) {
  289. $constants = $currentResult->getConstants();
  290. $last = end($constants);
  291. $value = ($last !== false) ? $last + 1 : 0;
  292. } else {
  293. $value = $tokenValue;
  294. }
  295.  
  296. $currentResult->setItem($name, $value);
  297. } else {
  298. $errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
  299. }
  300. break;
  301. }
  302. }
  303. }
  304.  
  305. return $result;
  306. }
  307.  
  308. private function cleanUp($input) {
  309. return trim($input);
  310. }
  311.  
  312. public function __construct(ILexer $lexer, INameValidator $nameValidator, IErrorManager $errorManager) {
  313. parent::__construct($errorManager);
  314.  
  315. $this->lexer = $lexer;
  316. $this->nameValidator = $nameValidator;
  317. }
  318. }
  319.  
  320. class ParseResult extends CompilerElement implements IParseResult {
  321. private $definition;
  322. private $name;
  323. private $constants = array();
  324.  
  325. public function getDefinition() {
  326. return $this->definition;
  327. }
  328.  
  329. public function getName() {
  330. return $this->name;
  331. }
  332.  
  333. public function getConstants() {
  334. return $this->constants;
  335. }
  336.  
  337. public function setItem($name, $value) {
  338. if (!is_string($name)) {
  339. $this->getErrorManager()->ariseFatal('Enumeration item name can only be string.');
  340. }
  341. if (!is_numeric($value)) {
  342. $this->getErrorManager()->ariseFatal('Enumeration item can only hold numeric value.');
  343. }
  344.  
  345. $this->constants[$name] = (ctype_digit($value) === true) ? (int)$value : (double)$value;
  346. }
  347.  
  348. public function setName($name) {
  349. if (!is_string($name)) {
  350. $this->getErrorManager()->ariseFatal(get_class($this).'::setName expects parameter 1 to be string. '.gettype($name).' given.');
  351. }
  352.  
  353. $this->name = $name;
  354. }
  355.  
  356. public function __construct($definition, IErrorManager $errorManager) {
  357. parent::__construct($errorManager);
  358.  
  359. if (!is_string($definition)) {
  360. $this->getErrorManager()->ariseFatal(get_class($this).'::__construct expects parameter 1 to be string. '.gettype($definition).' given.');
  361. }
  362.  
  363. $this->definition = $definition;
  364. }
  365. }
  366.  
  367. abstract class CompilerElement {
  368. private $errorManager;
  369.  
  370. protected function getErrorManager() {
  371. return $this->errorManager;
  372. }
  373.  
  374. protected function __construct(IErrorManager $errorManager) {
  375. $this->errorManager = $errorManager;
  376. }
  377. }
  378.  
  379. class Enum2PhpCompiler implements ICompiler {
  380. const INDENTATION_CHAR = "\t";
  381.  
  382. private $parser;
  383.  
  384. public function compile($input) {
  385. $parseResult = $this->parser->parse($input);
  386.  
  387. $return = '';
  388.  
  389. foreach ($parseResult as $result) {
  390. $return .= $this->generateClass($result);
  391. }
  392.  
  393. return $return;
  394. }
  395.  
  396. private function generateClass(IParseResult $parseResult) {
  397. $return = $parseResult->getDefinition().' '.$parseResult->getName().' {'.PHP_EOL;
  398.  
  399. foreach ($parseResult->getConstants() as $name => $value) {
  400. $return .= Enum2PhpCompiler::INDENTATION_CHAR.'const '.$name.' = '.$value.';'.PHP_EOL;
  401. }
  402.  
  403. $return .= Enum2PhpCompiler::INDENTATION_CHAR.'private function __construct() {}'.PHP_EOL;
  404. $return .= '}'.PHP_EOL;
  405.  
  406. return $return;
  407. }
  408.  
  409. public function __construct(IParser $parser) {
  410. $this->parser = $parser;
  411. $this->parser->setKeywords(array('enum' => 'final class'));
  412. }
  413. }
  414.  
  415. class ErrorManager implements IErrorManager {
  416. public function ariseFatal($message) {
  417. $this->triggerError($message, E_USER_ERROR);
  418. }
  419.  
  420. public function ariseWarning($message) {
  421. $this->triggerError($message, E_USER_WARNING);
  422. }
  423.  
  424. public function ariseNotice($message) {
  425. $this->triggerError($message, E_USER_NOTICE);
  426. }
  427.  
  428. private function triggerError($message, $type) {
  429. trigger_error((string)$message, (int)$type);
  430. }
  431. }
  432.  
  433. $errorManager = new ErrorManager();
  434. $nameValidator = new NameValidator($errorManager);
  435. $lexer = new Lexer($errorManager);
  436. $parser = new Parser($lexer, $nameValidator, $errorManager);
  437. $compiler = new Enum2PhpCompiler($parser);
  438.  
  439. $enum = <<<PHP
  440.   enum MyEnum {
  441. Item1 = 1,
  442. Item2 = 5,
  443. Item3
  444. }
  445.  
  446. enum AnotherEnum {
  447. Item
  448. }
  449. PHP;
  450.  
  451. eval($compiler->compile($enum));
  452.  
  453. var_dump(MyEnum::Item1, MyEnum::Item2, MyEnum::Item3, AnotherEnum::Item);
Success #stdin #stdout 0.01s 20568KB
stdin
Standard input is empty
stdout
int(1)
int(5)
int(6)
int(0)