Completed
Push — master ( 86484b...f97b0a )
by stéphane
05:27
created

Loader::needsSpecialProcess()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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