Bez popisu

Parser.php 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. namespace Dotenv;
  3. use Dotenv\Exception\InvalidFileException;
  4. use Dotenv\Regex\Regex;
  5. class Parser
  6. {
  7. const INITIAL_STATE = 0;
  8. const UNQUOTED_STATE = 1;
  9. const QUOTED_STATE = 2;
  10. const ESCAPE_STATE = 3;
  11. const WHITESPACE_STATE = 4;
  12. const COMMENT_STATE = 5;
  13. /**
  14. * Parse the given environment variable entry into a name and value.
  15. *
  16. * @param string $entry
  17. *
  18. * @throws \Dotenv\Exception\InvalidFileException
  19. *
  20. * @return array
  21. */
  22. public static function parse($entry)
  23. {
  24. list($name, $value) = self::splitStringIntoParts($entry);
  25. return [self::parseName($name), self::parseValue($value)];
  26. }
  27. /**
  28. * Split the compound string into parts.
  29. *
  30. * @param string $line
  31. *
  32. * @throws \Dotenv\Exception\InvalidFileException
  33. *
  34. * @return array
  35. */
  36. private static function splitStringIntoParts($line)
  37. {
  38. $name = $line;
  39. $value = null;
  40. if (strpos($line, '=') !== false) {
  41. list($name, $value) = array_map('trim', explode('=', $line, 2));
  42. }
  43. if ($name === '') {
  44. throw new InvalidFileException(
  45. self::getErrorMessage('an unexpected equals', $line)
  46. );
  47. }
  48. return [$name, $value];
  49. }
  50. /**
  51. * Strips quotes and the optional leading "export " from the variable name.
  52. *
  53. * @param string $name
  54. *
  55. * @throws \Dotenv\Exception\InvalidFileException
  56. *
  57. * @return string
  58. */
  59. private static function parseName($name)
  60. {
  61. $name = trim(str_replace(['export ', '\'', '"'], '', $name));
  62. if (!self::isValidName($name)) {
  63. throw new InvalidFileException(
  64. self::getErrorMessage('an invalid name', $name)
  65. );
  66. }
  67. return $name;
  68. }
  69. /**
  70. * Is the given variable name valid?
  71. *
  72. * @param string $name
  73. *
  74. * @return bool
  75. */
  76. private static function isValidName($name)
  77. {
  78. return Regex::match('~\A[a-zA-Z0-9_.]+\z~', $name)->success()->getOrElse(0) === 1;
  79. }
  80. /**
  81. * Strips quotes and comments from the environment variable value.
  82. *
  83. * @param string|null $value
  84. *
  85. * @throws \Dotenv\Exception\InvalidFileException
  86. *
  87. * @return string|null
  88. */
  89. private static function parseValue($value)
  90. {
  91. if ($value === null || trim($value) === '') {
  92. return $value;
  93. }
  94. $result = array_reduce(str_split($value), function ($data, $char) use ($value) {
  95. switch ($data[1]) {
  96. case self::INITIAL_STATE:
  97. if ($char === '"' || $char === '\'') {
  98. return [$data[0], self::QUOTED_STATE];
  99. } elseif ($char === '#') {
  100. return [$data[0], self::COMMENT_STATE];
  101. } else {
  102. return [$data[0].$char, self::UNQUOTED_STATE];
  103. }
  104. case self::UNQUOTED_STATE:
  105. if ($char === '#') {
  106. return [$data[0], self::COMMENT_STATE];
  107. } elseif (ctype_space($char)) {
  108. return [$data[0], self::WHITESPACE_STATE];
  109. } else {
  110. return [$data[0].$char, self::UNQUOTED_STATE];
  111. }
  112. case self::QUOTED_STATE:
  113. if ($char === $value[0]) {
  114. return [$data[0], self::WHITESPACE_STATE];
  115. } elseif ($char === '\\') {
  116. return [$data[0], self::ESCAPE_STATE];
  117. } else {
  118. return [$data[0].$char, self::QUOTED_STATE];
  119. }
  120. case self::ESCAPE_STATE:
  121. if ($char === $value[0] || $char === '\\') {
  122. return [$data[0].$char, self::QUOTED_STATE];
  123. } elseif (in_array($char, ['f', 'n', 'r', 't', 'v'], true)) {
  124. return [$data[0].stripcslashes('\\'.$char), self::QUOTED_STATE];
  125. } else {
  126. throw new InvalidFileException(
  127. self::getErrorMessage('an unexpected escape sequence', $value)
  128. );
  129. }
  130. case self::WHITESPACE_STATE:
  131. if ($char === '#') {
  132. return [$data[0], self::COMMENT_STATE];
  133. } elseif (!ctype_space($char)) {
  134. throw new InvalidFileException(
  135. self::getErrorMessage('unexpected whitespace', $value)
  136. );
  137. } else {
  138. return [$data[0], self::WHITESPACE_STATE];
  139. }
  140. case self::COMMENT_STATE:
  141. return [$data[0], self::COMMENT_STATE];
  142. }
  143. }, ['', self::INITIAL_STATE]);
  144. if ($result[1] === self::QUOTED_STATE || $result[1] === self::ESCAPE_STATE) {
  145. throw new InvalidFileException(
  146. self::getErrorMessage('a missing closing quote', $value)
  147. );
  148. }
  149. return $result[0];
  150. }
  151. /**
  152. * Generate a friendly error message.
  153. *
  154. * @param string $cause
  155. * @param string $subject
  156. *
  157. * @return string
  158. */
  159. private static function getErrorMessage($cause, $subject)
  160. {
  161. return sprintf(
  162. 'Failed to parse dotenv file due to %s. Failed at [%s].',
  163. $cause,
  164. strtok($subject, "\n")
  165. );
  166. }
  167. }