Completed
Push — master ( 3eacba...3b6a73 )
by stéphane
02:25
created

Loader::onError()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 10
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 2
crap 3
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 10
    public function __construct($absolutePath = null, $options = null, $debug = 0)
49
    {
50 10
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
51 10
        $this->_options = is_int($options) ? $options : $this->_options;
52 10
        if (is_string($absolutePath)) {
53 1
            $this->load($absolutePath);
54
        }
55 10
    }
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 3
    public function load(string $absolutePath):Loader
67
    {
68 3
        if (!file_exists($absolutePath)) {
69 2
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
70
        }
71 1
        $this->filePath = $absolutePath;
72 1
        $adle = "auto_detect_line_endings";
73 1
        $prevADLE = ini_get($adle);
74 1
        !$prevADLE && ini_set($adle, "true");
75 1
        $content = file($absolutePath, FILE_IGNORE_NEW_LINES);
76 1
        !$prevADLE && ini_set($adle, "false");
77 1
        if (is_bool($content)) {
78
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
79
        }
80 1
        $this->content = $content;
81 1
        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 1
    private function getSourceGenerator($strContent = null):\Generator
93
    {
94 1
        $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 1
        if (!is_array($source) || !count($source)) throw new \Exception(self::EXCEPTION_LINE_SPLIT);
97 1
        foreach ($source as $key => $value) {
98 1
            yield ++$key => $value;
99
        }
100 1
    }
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 1
    public function parse($strContent = null)
113
    {
114 1
        $generator = $this->getSourceGenerator($strContent);
115 1
        $previous = $root = new NodeRoot();
116
        try {
117 1
            foreach ($generator as $lineNb => $lineString) {
118 1
                $node = NodeFactory::get($lineString, $lineNb);
119 1
                if ($this->_debug === 1) echo get_class($node)."\n";
120 1
                if ($this->needsSpecialProcess($node, $previous)) continue;
121 1
                $this->attachBlankLines($previous);
122 1
                switch ($node->indent <=> $previous->indent) {
123
                    case -1: $target = $previous->getTargetOnLessIndent($node);
124
                        break;
125 1
                    case 0:  $target = $previous->getTargetOnEqualIndent($node);
126 1
                        break;
127 1
                    default: $target = $previous->getTargetOnMoreIndent($node);
128
                }
129 1
                $previous = $target->add($node);
130
            }
131 1
            $this->attachBlankLines($previous);
132 1
            if ($this->_debug === 1){
133
                return;
134
            }
135 1
            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 2
    public function attachBlankLines(Node &$previous)
151
    {
152 2
        foreach ($this->_blankBuffer as $blankNode) {
153 1
            if ($blankNode !== $previous) {
154 1
                $blankNode->getParent()->add($blankNode);
155
            }
156
        }
157 2
        $this->_blankBuffer = [];
158 2
    }
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 2
    public function needsSpecialProcess(Node $current, Node &$previous):bool
170
    {
171 2
        $deepest = $previous->getDeepestNode();
172 2
        if ($deepest instanceof NodePartial) {
173 1
            return $deepest->specialProcess($current,  $this->_blankBuffer);
174 2
        } elseif(!($current instanceof NodePartial)) {
175 2
            return $current->specialProcess($previous, $this->_blankBuffer);
176
        }
177 1
        return false;
178
    }
179
180 2
    public function onError(object $e, \Generator $generator)
181
    {
182 2
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
183 2
        $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
184 2
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
185 1
            self::$error = $message;
186 1
            return null;
187
        }
188 1
        $line = $generator->key() ?? 'X';
189 1
        throw new \Exception($message." for $file:".$line, 1, $e);
190
    }
191
}
192