Issues (13)

src/Loader.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Dallgoot\Yaml;
6
7
use Dallgoot\Yaml\Nodes;
8
use Dallgoot\Yaml\Nodes\Generic\NodeGeneric;
9
use Dallgoot\Yaml\Types\YamlObject;
10
11
/**
12
 * Process reading a Yaml Content (loading file if required)
13
 * and for each line affects appropriate NodeType
14
 * and attach to proper parent Node
15
 * ie. constructs a tree of Nodes with a NodeRoot as first Node
16
 *
17
 * @author  Stéphane Rebai <[email protected]>
18
 * @license Apache 2.0
19
 * @link    https://github.com/dallgoot/yaml
20
 */
21
final class Loader
22
{
23
    //public
24
    public static ?string $error;
25
    public const IGNORE_DIRECTIVES     = 0b0001; //DONT include_directive
26
    public const IGNORE_COMMENTS       = 0b0010; //DONT include_comments
27
    public const NO_PARSING_EXCEPTIONS = 0b0100; //DONT throw Exception on parsing errors
28
    public const NO_OBJECT_FOR_DATE    = 0b1000; //DONT import date strings as dateTime Object
29
30
    //private
31
    private ?\SplFixedArray $content = null;
32
    private ?string $filePath = null;
33
    private int $_debug = 0;
34
    private int $_options = 0;
35
    private array $_blankBuffer = [];
36
37
    //Exceptions messages
38
    private const INVALID_VALUE        = self::class . ": at line %d";
39
    private const EXCEPTION_NO_FILE    = self::class . ": file '%s' does not exists (or path is incorrect?)";
40
    private const EXCEPTION_READ_ERROR = self::class . ": file '%s' failed to be loaded (permission denied ?)";
41
    private const EXCEPTION_LINE_SPLIT = self::class . ": content is not a string (maybe a file error?)";
42
43
    /**
44
     * Loader constructor
45
     */
46 11
    public function __construct(?string $absolutePath = null, ?int $options = null, ?int $debug = 0)
47
    {
48 11
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
49 11
        $this->_options = is_int($options) ? $options : $this->_options;
50 11
        if (is_string($absolutePath)) {
51 1
            $this->load($absolutePath);
52
        }
53
    }
54
55
    /**
56
     * Load a file and save its content as $content
57
     *
58
     * @param string $absolutePath The absolute path of a file
59
     *
60
     * @throws \Exception if file don't exist OR reading failed
61
     *
62
     * @return self  ( returns the same Loader  )
63
     */
64 3
    public function load(string $absolutePath): Loader
65
    {
66 3
        if (!file_exists($absolutePath)) {
67 2
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
68
        }
69 1
        $this->filePath = $absolutePath;
70
71 1
        $content = @file($absolutePath, FILE_IGNORE_NEW_LINES);
72
73 1
        if (is_bool($content)) {
74
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
75
        }
76 1
        $this->content = \SplFixedArray::fromArray($content, false);
77 1
        return $this;
78
    }
79
80
    /**
81
     * Gets the source iterator.
82
     *
83
     * @param string|null $strContent  The string content
84
     *
85
     * @throws \Exception if self::content is empty or splitting on linefeed has failed
86
     * @return \Generator  The source iterator.
87
     */
88 3
    private function getSourceGenerator(?string $strContent = null): \Generator
89
    {
90 3
        if (is_null($strContent)) {
91 1
            if(is_null($this->content)) {
92 1
                throw new \Exception(self::EXCEPTION_LINE_SPLIT);
93
            }else {
94
                $source = $this->content;
95
            }
96
        } else {
97 2
            $simplerLineFeeds = preg_replace('/(\r\n|\r)/', "\n", (string) $strContent);
98 2
            $source = preg_split("/\n/m", $simplerLineFeeds, 0, \PREG_SPLIT_DELIM_CAPTURE);
99 2
            if (!count($source)) {
100
                throw new \Exception(self::EXCEPTION_LINE_SPLIT);
101
            }
102 2
            $source = \SplFixedArray::fromArray($source, false);
103
        }
104 2
        foreach ($source as $key => $value) {
105 2
            yield ++$key => $value;
106
        }
107
    }
108
109
    /**
110
     * Parse Yaml lines into a hierarchy of Node
111
     *
112
     * @param ?string $strContent The Yaml string or null to parse loaded content
113
     *
114
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
115
     * @throws \ParseError  if any error during parsing or building
116
     *
117
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
118
     */
119 2
    public function parse(?string $strContent = null)
120
    {
121 2
        if(!is_null($strContent)) {
122 2
            $this->content = null;
123
        }
124 2
        $generator = $this->getSourceGenerator($strContent);
125 2
        $previous = $root = new Nodes\Root();
126 2
        $debugNodeFactory = $this->_debug === 1;
127
        try {
128 2
            foreach ($generator as $lineNB => $lineString) {
129 2
                $node = NodeFactory::get($lineString, $lineNB, $debugNodeFactory);
130 2
                if ($this->needsSpecialProcess($node, $previous)) continue;
131 2
                $this->_attachBlankLines($previous);
132 2
                $target = match ($node->indent <=> $previous->indent) {
133 1
                    -1 => $previous->getTargetOnLessIndent($node),
134 1
                    0  => $previous->getTargetOnEqualIndent($node),
135 2
                    1  => $previous->getTargetOnMoreIndent($node)
136 2
                };
137 2
                $previous = $target->add($node);
138
            }
139 2
            $this->_attachBlankLines($previous);
140 2
            return (new Builder($this->_options, $this->_debug))->buildContent($root);
141 1
        } catch (\Throwable $e) {
142 1
            $this->onError($e);
143
        }
144
    }
145
146
147
    /**
148
     * Attach blank (empty) Nodes saved in $_blankBuffer to their parent (it means they are meaningful content)
149
     *
150
     * @param NodeGeneric  $previous   The previous Node
151
     *
152
     * @return null
153
     */
154 3
    private function _attachBlankLines(NodeGeneric $previous)
155
    {
156 3
        foreach ($this->_blankBuffer as $blankNode) {
157 1
            if ($blankNode !== $previous) {
158 1
                $blankNode->getParent()->add($blankNode);
159
            }
160
        }
161 3
        $this->_blankBuffer = [];
162
    }
163
164
    /**
165
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
166
     *
167
     * @param NodeGeneric   $previous   The previous Node
168
     *
169
     * @return boolean  if True self::parse skips changing previous and adding to parent
170
     * @see self::parse
171
     */
172 3
    private function needsSpecialProcess(NodeGeneric $current, NodeGeneric $previous): bool
173
    {
174 3
        $deepest = $previous->getDeepestNode();
175 3
        if ($deepest instanceof Nodes\Partial) {
176 1
            return $deepest->specialProcess($current,  $this->_blankBuffer);
177 3
        } elseif (!($current instanceof Nodes\Partial)) {
178 3
            return $current->specialProcess($previous, $this->_blankBuffer);
179
            if($current instanceof Nodes\Comment) {
0 ignored issues
show
IfNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
180
                return true;
181
            }
182
        }
183 1
        return false;
184
    }
185
186 2
    private function onError(\Throwable $e)
187
    {
188 2
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
189 2
        $message = $e->getMessage() . "\n " . $e->getFile() . ":" . $e->getLine();
190 2
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
191 1
            self::$error = $message;
192 1
            return null;
193
        }
194 1
        throw new \Exception($message . " for $file:" . $e->getLine(), 1, $e);
195
    }
196
}
197