FormulaParser.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Calculation;
  3. /**
  4. * PARTLY BASED ON:
  5. * Copyright (c) 2007 E. W. Bachtal, Inc.
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  8. * and associated documentation files (the "Software"), to deal in the Software without restriction,
  9. * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  10. * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
  11. * subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in all copies or substantial
  14. * portions of the Software.
  15. *
  16. * The software is provided "as is", without warranty of any kind, express or implied, including but not
  17. * limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In
  18. * no event shall the authors or copyright holders be liable for any claim, damages or other liability,
  19. * whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
  20. * software or the use or other dealings in the software.
  21. *
  22. * http://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
  23. * http://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
  24. */
  25. class FormulaParser
  26. {
  27. // Character constants
  28. const QUOTE_DOUBLE = '"';
  29. const QUOTE_SINGLE = '\'';
  30. const BRACKET_CLOSE = ']';
  31. const BRACKET_OPEN = '[';
  32. const BRACE_OPEN = '{';
  33. const BRACE_CLOSE = '}';
  34. const PAREN_OPEN = '(';
  35. const PAREN_CLOSE = ')';
  36. const SEMICOLON = ';';
  37. const WHITESPACE = ' ';
  38. const COMMA = ',';
  39. const ERROR_START = '#';
  40. const OPERATORS_SN = '+-';
  41. const OPERATORS_INFIX = '+-*/^&=><';
  42. const OPERATORS_POSTFIX = '%';
  43. /**
  44. * Formula.
  45. *
  46. * @var string
  47. */
  48. private $formula;
  49. /**
  50. * Tokens.
  51. *
  52. * @var FormulaToken[]
  53. */
  54. private $tokens = [];
  55. /**
  56. * Create a new FormulaParser.
  57. *
  58. * @param string $pFormula Formula to parse
  59. *
  60. * @throws Exception
  61. */
  62. public function __construct($pFormula = '')
  63. {
  64. // Check parameters
  65. if ($pFormula === null) {
  66. throw new Exception('Invalid parameter passed: formula');
  67. }
  68. // Initialise values
  69. $this->formula = trim($pFormula);
  70. // Parse!
  71. $this->parseToTokens();
  72. }
  73. /**
  74. * Get Formula.
  75. *
  76. * @return string
  77. */
  78. public function getFormula()
  79. {
  80. return $this->formula;
  81. }
  82. /**
  83. * Get Token.
  84. *
  85. * @param int $pId Token id
  86. *
  87. * @throws Exception
  88. *
  89. * @return string
  90. */
  91. public function getToken($pId = 0)
  92. {
  93. if (isset($this->tokens[$pId])) {
  94. return $this->tokens[$pId];
  95. }
  96. throw new Exception("Token with id $pId does not exist.");
  97. }
  98. /**
  99. * Get Token count.
  100. *
  101. * @return int
  102. */
  103. public function getTokenCount()
  104. {
  105. return count($this->tokens);
  106. }
  107. /**
  108. * Get Tokens.
  109. *
  110. * @return FormulaToken[]
  111. */
  112. public function getTokens()
  113. {
  114. return $this->tokens;
  115. }
  116. /**
  117. * Parse to tokens.
  118. */
  119. private function parseToTokens()
  120. {
  121. // No attempt is made to verify formulas; assumes formulas are derived from Excel, where
  122. // they can only exist if valid; stack overflows/underflows sunk as nulls without exceptions.
  123. // Check if the formula has a valid starting =
  124. $formulaLength = strlen($this->formula);
  125. if ($formulaLength < 2 || $this->formula[0] != '=') {
  126. return;
  127. }
  128. // Helper variables
  129. $tokens1 = $tokens2 = $stack = [];
  130. $inString = $inPath = $inRange = $inError = false;
  131. $token = $previousToken = $nextToken = null;
  132. $index = 1;
  133. $value = '';
  134. $ERRORS = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A'];
  135. $COMPARATORS_MULTI = ['>=', '<=', '<>'];
  136. while ($index < $formulaLength) {
  137. // state-dependent character evaluation (order is important)
  138. // double-quoted strings
  139. // embeds are doubled
  140. // end marks token
  141. if ($inString) {
  142. if ($this->formula[$index] == self::QUOTE_DOUBLE) {
  143. if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_DOUBLE)) {
  144. $value .= self::QUOTE_DOUBLE;
  145. ++$index;
  146. } else {
  147. $inString = false;
  148. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_TEXT);
  149. $value = '';
  150. }
  151. } else {
  152. $value .= $this->formula[$index];
  153. }
  154. ++$index;
  155. continue;
  156. }
  157. // single-quoted strings (links)
  158. // embeds are double
  159. // end does not mark a token
  160. if ($inPath) {
  161. if ($this->formula[$index] == self::QUOTE_SINGLE) {
  162. if ((($index + 2) <= $formulaLength) && ($this->formula[$index + 1] == self::QUOTE_SINGLE)) {
  163. $value .= self::QUOTE_SINGLE;
  164. ++$index;
  165. } else {
  166. $inPath = false;
  167. }
  168. } else {
  169. $value .= $this->formula[$index];
  170. }
  171. ++$index;
  172. continue;
  173. }
  174. // bracked strings (R1C1 range index or linked workbook name)
  175. // no embeds (changed to "()" by Excel)
  176. // end does not mark a token
  177. if ($inRange) {
  178. if ($this->formula[$index] == self::BRACKET_CLOSE) {
  179. $inRange = false;
  180. }
  181. $value .= $this->formula[$index];
  182. ++$index;
  183. continue;
  184. }
  185. // error values
  186. // end marks a token, determined from absolute list of values
  187. if ($inError) {
  188. $value .= $this->formula[$index];
  189. ++$index;
  190. if (in_array($value, $ERRORS)) {
  191. $inError = false;
  192. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND, FormulaToken::TOKEN_SUBTYPE_ERROR);
  193. $value = '';
  194. }
  195. continue;
  196. }
  197. // scientific notation check
  198. if (strpos(self::OPERATORS_SN, $this->formula[$index]) !== false) {
  199. if (strlen($value) > 1) {
  200. if (preg_match('/^[1-9]{1}(\\.\\d+)?E{1}$/', $this->formula[$index]) != 0) {
  201. $value .= $this->formula[$index];
  202. ++$index;
  203. continue;
  204. }
  205. }
  206. }
  207. // independent character evaluation (order not important)
  208. // establish state-dependent character evaluations
  209. if ($this->formula[$index] == self::QUOTE_DOUBLE) {
  210. if (strlen($value) > 0) {
  211. // unexpected
  212. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
  213. $value = '';
  214. }
  215. $inString = true;
  216. ++$index;
  217. continue;
  218. }
  219. if ($this->formula[$index] == self::QUOTE_SINGLE) {
  220. if (strlen($value) > 0) {
  221. // unexpected
  222. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
  223. $value = '';
  224. }
  225. $inPath = true;
  226. ++$index;
  227. continue;
  228. }
  229. if ($this->formula[$index] == self::BRACKET_OPEN) {
  230. $inRange = true;
  231. $value .= self::BRACKET_OPEN;
  232. ++$index;
  233. continue;
  234. }
  235. if ($this->formula[$index] == self::ERROR_START) {
  236. if (strlen($value) > 0) {
  237. // unexpected
  238. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
  239. $value = '';
  240. }
  241. $inError = true;
  242. $value .= self::ERROR_START;
  243. ++$index;
  244. continue;
  245. }
  246. // mark start and end of arrays and array rows
  247. if ($this->formula[$index] == self::BRACE_OPEN) {
  248. if (strlen($value) > 0) {
  249. // unexpected
  250. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_UNKNOWN);
  251. $value = '';
  252. }
  253. $tmp = new FormulaToken('ARRAY', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
  254. $tokens1[] = $tmp;
  255. $stack[] = clone $tmp;
  256. $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
  257. $tokens1[] = $tmp;
  258. $stack[] = clone $tmp;
  259. ++$index;
  260. continue;
  261. }
  262. if ($this->formula[$index] == self::SEMICOLON) {
  263. if (strlen($value) > 0) {
  264. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  265. $value = '';
  266. }
  267. $tmp = array_pop($stack);
  268. $tmp->setValue('');
  269. $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
  270. $tokens1[] = $tmp;
  271. $tmp = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT);
  272. $tokens1[] = $tmp;
  273. $tmp = new FormulaToken('ARRAYROW', FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
  274. $tokens1[] = $tmp;
  275. $stack[] = clone $tmp;
  276. ++$index;
  277. continue;
  278. }
  279. if ($this->formula[$index] == self::BRACE_CLOSE) {
  280. if (strlen($value) > 0) {
  281. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  282. $value = '';
  283. }
  284. $tmp = array_pop($stack);
  285. $tmp->setValue('');
  286. $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
  287. $tokens1[] = $tmp;
  288. $tmp = array_pop($stack);
  289. $tmp->setValue('');
  290. $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
  291. $tokens1[] = $tmp;
  292. ++$index;
  293. continue;
  294. }
  295. // trim white-space
  296. if ($this->formula[$index] == self::WHITESPACE) {
  297. if (strlen($value) > 0) {
  298. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  299. $value = '';
  300. }
  301. $tokens1[] = new FormulaToken('', FormulaToken::TOKEN_TYPE_WHITESPACE);
  302. ++$index;
  303. while (($this->formula[$index] == self::WHITESPACE) && ($index < $formulaLength)) {
  304. ++$index;
  305. }
  306. continue;
  307. }
  308. // multi-character comparators
  309. if (($index + 2) <= $formulaLength) {
  310. if (in_array(substr($this->formula, $index, 2), $COMPARATORS_MULTI)) {
  311. if (strlen($value) > 0) {
  312. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  313. $value = '';
  314. }
  315. $tokens1[] = new FormulaToken(substr($this->formula, $index, 2), FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_LOGICAL);
  316. $index += 2;
  317. continue;
  318. }
  319. }
  320. // standard infix operators
  321. if (strpos(self::OPERATORS_INFIX, $this->formula[$index]) !== false) {
  322. if (strlen($value) > 0) {
  323. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  324. $value = '';
  325. }
  326. $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORINFIX);
  327. ++$index;
  328. continue;
  329. }
  330. // standard postfix operators (only one)
  331. if (strpos(self::OPERATORS_POSTFIX, $this->formula[$index]) !== false) {
  332. if (strlen($value) > 0) {
  333. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  334. $value = '';
  335. }
  336. $tokens1[] = new FormulaToken($this->formula[$index], FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX);
  337. ++$index;
  338. continue;
  339. }
  340. // start subexpression or function
  341. if ($this->formula[$index] == self::PAREN_OPEN) {
  342. if (strlen($value) > 0) {
  343. $tmp = new FormulaToken($value, FormulaToken::TOKEN_TYPE_FUNCTION, FormulaToken::TOKEN_SUBTYPE_START);
  344. $tokens1[] = $tmp;
  345. $stack[] = clone $tmp;
  346. $value = '';
  347. } else {
  348. $tmp = new FormulaToken('', FormulaToken::TOKEN_TYPE_SUBEXPRESSION, FormulaToken::TOKEN_SUBTYPE_START);
  349. $tokens1[] = $tmp;
  350. $stack[] = clone $tmp;
  351. }
  352. ++$index;
  353. continue;
  354. }
  355. // function, subexpression, or array parameters, or operand unions
  356. if ($this->formula[$index] == self::COMMA) {
  357. if (strlen($value) > 0) {
  358. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  359. $value = '';
  360. }
  361. $tmp = array_pop($stack);
  362. $tmp->setValue('');
  363. $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
  364. $stack[] = $tmp;
  365. if ($tmp->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) {
  366. $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_UNION);
  367. } else {
  368. $tokens1[] = new FormulaToken(',', FormulaToken::TOKEN_TYPE_ARGUMENT);
  369. }
  370. ++$index;
  371. continue;
  372. }
  373. // stop subexpression
  374. if ($this->formula[$index] == self::PAREN_CLOSE) {
  375. if (strlen($value) > 0) {
  376. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  377. $value = '';
  378. }
  379. $tmp = array_pop($stack);
  380. $tmp->setValue('');
  381. $tmp->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_STOP);
  382. $tokens1[] = $tmp;
  383. ++$index;
  384. continue;
  385. }
  386. // token accumulation
  387. $value .= $this->formula[$index];
  388. ++$index;
  389. }
  390. // dump remaining accumulation
  391. if (strlen($value) > 0) {
  392. $tokens1[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERAND);
  393. }
  394. // move tokenList to new set, excluding unnecessary white-space tokens and converting necessary ones to intersections
  395. $tokenCount = count($tokens1);
  396. for ($i = 0; $i < $tokenCount; ++$i) {
  397. $token = $tokens1[$i];
  398. if (isset($tokens1[$i - 1])) {
  399. $previousToken = $tokens1[$i - 1];
  400. } else {
  401. $previousToken = null;
  402. }
  403. if (isset($tokens1[$i + 1])) {
  404. $nextToken = $tokens1[$i + 1];
  405. } else {
  406. $nextToken = null;
  407. }
  408. if ($token === null) {
  409. continue;
  410. }
  411. if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) {
  412. $tokens2[] = $token;
  413. continue;
  414. }
  415. if ($previousToken === null) {
  416. continue;
  417. }
  418. if (!(
  419. (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
  420. (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
  421. ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
  422. )) {
  423. continue;
  424. }
  425. if ($nextToken === null) {
  426. continue;
  427. }
  428. if (!(
  429. (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
  430. (($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) && ($nextToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_START)) ||
  431. ($nextToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)
  432. )) {
  433. continue;
  434. }
  435. $tokens2[] = new FormulaToken($value, FormulaToken::TOKEN_TYPE_OPERATORINFIX, FormulaToken::TOKEN_SUBTYPE_INTERSECTION);
  436. }
  437. // move tokens to final list, switching infix "-" operators to prefix when appropriate, switching infix "+" operators
  438. // to noop when appropriate, identifying operand and infix-operator subtypes, and pulling "@" from function names
  439. $this->tokens = [];
  440. $tokenCount = count($tokens2);
  441. for ($i = 0; $i < $tokenCount; ++$i) {
  442. $token = $tokens2[$i];
  443. if (isset($tokens2[$i - 1])) {
  444. $previousToken = $tokens2[$i - 1];
  445. } else {
  446. $previousToken = null;
  447. }
  448. if (isset($tokens2[$i + 1])) {
  449. $nextToken = $tokens2[$i + 1];
  450. } else {
  451. $nextToken = null;
  452. }
  453. if ($token === null) {
  454. continue;
  455. }
  456. if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') {
  457. if ($i == 0) {
  458. $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX);
  459. } elseif ((($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) &&
  460. ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
  461. (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) &&
  462. ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
  463. ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) ||
  464. ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)) {
  465. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
  466. } else {
  467. $token->setTokenType(FormulaToken::TOKEN_TYPE_OPERATORPREFIX);
  468. }
  469. $this->tokens[] = $token;
  470. continue;
  471. }
  472. if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '+') {
  473. if ($i == 0) {
  474. continue;
  475. } elseif ((($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) &&
  476. ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
  477. (($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_SUBEXPRESSION) &&
  478. ($previousToken->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_STOP)) ||
  479. ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORPOSTFIX) ||
  480. ($previousToken->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND)) {
  481. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
  482. } else {
  483. continue;
  484. }
  485. $this->tokens[] = $token;
  486. continue;
  487. }
  488. if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX &&
  489. $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING) {
  490. if (strpos('<>=', substr($token->getValue(), 0, 1)) !== false) {
  491. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL);
  492. } elseif ($token->getValue() == '&') {
  493. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_CONCATENATION);
  494. } else {
  495. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_MATH);
  496. }
  497. $this->tokens[] = $token;
  498. continue;
  499. }
  500. if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERAND &&
  501. $token->getTokenSubType() == FormulaToken::TOKEN_SUBTYPE_NOTHING) {
  502. if (!is_numeric($token->getValue())) {
  503. if (strtoupper($token->getValue()) == 'TRUE' || strtoupper($token->getValue()) == 'FALSE') {
  504. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_LOGICAL);
  505. } else {
  506. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_RANGE);
  507. }
  508. } else {
  509. $token->setTokenSubType(FormulaToken::TOKEN_SUBTYPE_NUMBER);
  510. }
  511. $this->tokens[] = $token;
  512. continue;
  513. }
  514. if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_FUNCTION) {
  515. if (strlen($token->getValue()) > 0) {
  516. if (substr($token->getValue(), 0, 1) == '@') {
  517. $token->setValue(substr($token->getValue(), 1));
  518. }
  519. }
  520. }
  521. $this->tokens[] = $token;
  522. }
  523. }
  524. }