Passed
Push — master ( 9bf5f8...0872bb )
by stéphane
02:09
created

Loader::parse()   B

Complexity

Conditions 8
Paths 46

Size

Total Lines 26
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 26
rs 8.4444
c 0
b 0
f 0
cc 8
nc 46
nop 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Dallgoot\Yaml;
5
6
/**
7
 * TODO
8
 *
9
 * @author  Stéphane Rebai <[email protected]>
10
 * @license Apache 2.0
11
 * @link    TODO : url to specific online doc
12
 */
13
final class Loader
14
{
15
    //public
16
    /* @var null|string */
17
    public static $error;
18
    public const IGNORE_DIRECTIVES     = 1;//DONT include_directive
19
    public const IGNORE_COMMENTS       = 2;//DONT include_comments
20
    public const NO_PARSING_EXCEPTIONS = 4;//DONT throw Exception on parsing errors
21
    public const NO_OBJECT_FOR_DATE    = 8;//DONT import date strings as dateTime Object
22
23
    //private
24
    /* @var null|false|array */
25
    private $content;
26
    /* @var null|string */
27
    private $filePath;
28
    /* @var integer */
29
    private $_debug = 0;
30
    /* @var integer */
31
    private $_options = 0;
32
    /* @var array */
33
    private $_blankBuffer = [];
34
35
    //Exceptions messages
36
    private const INVALID_VALUE        = self::class.": at line %d";
37
    private const EXCEPTION_NO_FILE    = self::class.": file '%s' does not exists (or path is incorrect?)";
38
    private const EXCEPTION_READ_ERROR = self::class.": file '%s' failed to be loaded (permission denied ?)";
39
    private const EXCEPTION_LINE_SPLIT = self::class.": content is not a string (maybe a file error?)";
40
41
    /**
42
     * Loader constructor
43
     *
44
     * @param string|null       $absolutePath The file absolute path
45
     * @param int|null          $options      The options (bitmask as int value)
46
     * @param integer|bool|null $debug        The debug level as either boolean (true=1) or any integer
47
     */
48
    public function __construct($absolutePath = null, $options = null, $debug = 0)
49
    {
50
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
51
        $this->_options = is_int($options) ? $options : $this->_options;
52
        if (is_string($absolutePath)) {
53
            $this->load($absolutePath);
54
        }
55
    }
56
57
    /**
58
     * Load a file and save its content as $content
59
     *
60
     * @param string $absolutePath The absolute path of a file
61
     *
62
     * @throws \Exception if file don't exist OR reading failed
63
     *
64
     * @return self  ( returns the same Loader  )
65
     */
66
    public function load(string $absolutePath):Loader
67
    {
68
        if (!file_exists($absolutePath)) {
69
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
70
        }
71
        $this->filePath = $absolutePath;
72
        $adle = "auto_detect_line_endings";
73
        $prevADLE = ini_get($adle);
74
        !$prevADLE && ini_set($adle, "true");
75
        $content = file($absolutePath, FILE_IGNORE_NEW_LINES);
76
        !$prevADLE && ini_set($adle, "false");
77
        if (is_bool($content)) {
78
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
79
        }
80
        $this->content = $content;
81
        return $this;
82
    }
83
84
    /**
85
     * Gets the source iterator.
86
     *
87
     * @param string|null $strContent  The string content
88
     *
89
     * @throws \Exception if self::content is empty or splitting on linefeed has failed
90
     * @return \Generator  The source iterator.
91
     */
92
    private function getSourceGenerator($strContent = null):\Generator
93
    {
94
        $source = $this->content ?? preg_split("/\n/m", preg_replace('/(\r\n|\r)/', "\n", $strContent), 0, PREG_SPLIT_DELIM_CAPTURE);
95
        //TODO : be more permissive on $strContent values
96
        if (!is_array($source) || !count($source)) throw new \Exception(self::EXCEPTION_LINE_SPLIT);
97
        foreach ($source as $key => $value) {
98
            yield ++$key => $value;
99
        }
100
    }
101
102
    /**
103
     * Parse Yaml lines into a hierarchy of Node
104
     *
105
     * @param string $strContent The Yaml string or null to parse loaded content
106
     *
107
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
108
     * @throws \ParseError  if any error during parsing or building
109
     *
110
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
111
     */
112
    public function parse($strContent = null)
113
    {
114
        $generator = $this->getSourceGenerator($strContent);
115
        $previous = $root = new NodeRoot();
116
        try {
117
            foreach ($generator as $lineNb => $lineString) {
118
                $node = NodeFactory::get($lineString, $lineNb);
119
                if ($this->_debug === 1) echo get_class($node)."\n";
120
                if ($this->needsSpecialProcess($node, $previous)) continue;
121
                $this->attachBlankLines($previous);
122
                switch ($node->indent <=> $previous->indent) {
123
                    case -1: $target = $previous->getTargetOnLessIndent($node);
124
                        break;
125
                    case 0:  $target = $previous->getTargetOnEqualIndent($node);
126
                        break;
127
                    default: $target = $previous->getTargetOnMoreIndent($node);
128
                }
129
                $previous = $target->add($node);
130
            }
131
            $this->attachBlankLines($previous);
132
            if ($this->_debug === 1){
133
                return;
134
            }
135
            return Builder::buildContent($root, $this->_debug);
136
        } catch (\Error|\Exception|\ParseError $e) {
137
            $this->onError($e, $generator);
138
        }
139
    }
140
141
142
    /**
143
     * Attach blank(empty) Nodes savec in $blankBuffer to their parent (it means they are needed)
144
     *
145
     * @param array $emptyLines The empty lines
146
     * @param Node  $previous   The previous
147
     *
148
     * @return null
149
     */
150
    public function attachBlankLines(Node &$previous)
151
    {
152
        foreach ($this->_blankBuffer as $blankNode) {
153
            if ($blankNode !== $previous) {
154
                $blankNode->getParent()->add($blankNode);
155
            }
156
        }
157
        $this->_blankBuffer = [];
158
    }
159
160
    /**
161
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
162
     *
163
     * @param Node   $previous   The previous Node
164
     * @param array  $emptyLines The empty lines
165
     *
166
     * @return boolean  if True self::parse skips changing previous and adding to parent
167
     * @see self::parse
168
     */
169
    public function needsSpecialProcess(Node $current, Node &$previous):bool
170
    {
171
        $deepest = $previous->getDeepestNode();
172
        if ($deepest instanceof NodePartial) {
173
            return $deepest->specialProcess($current,  $this->_blankBuffer);
174
        } elseif(!($current instanceof NodePartial)) {
175
            return $current->specialProcess($previous, $this->_blankBuffer);
176
        }
177
        return false;
178
    }
179
180
    public function onError(object $e, $generator)
181
    {
182
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
183
        $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
184
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
185
            self::$error = $message;
186
            return null;
187
        }
188
        $line = $generator->key() ?? 'X';
189
        throw new \Exception($message." for $file:".$line, 1, $e);
190
    }
191
}
192