Passed
Push — master ( f81cc4...5281ab )
by stéphane
04:50
created

Loader   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 94.59%

Importance

Changes 0
Metric Value
eloc 80
dl 0
loc 185
ccs 70
cts 74
cp 0.9459
rs 9.76
c 0
b 0
f 0
wmc 33

7 Methods

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