Passed
Push — master ( dd077a...e2a7b5 )
by stéphane
05:33
created

Loader   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 173
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 28
eloc 70
dl 0
loc 173
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A load() 0 16 5
A getSourceGenerator() 0 7 4
A __construct() 0 6 4
A onError() 0 10 3
A parse() 0 22 6
A attachBlankLines() 0 8 3
A needsSpecialProcess() 0 9 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;///TODO: determine levels
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_int($debug) ? min($debug, 3) : 1;
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->needsSpecialProcess($node, $previous)) continue;
120
                $this->attachBlankLines($previous);
121
                switch ($node->indent <=> $previous->indent) {
122
                    case -1: $target = $previous->getTargetOnLessIndent($node);
123
                        break;
124
                    case 0:  $target = $previous->getTargetOnEqualIndent($node);
125
                        break;
126
                    default: $target = $previous->getTargetOnMoreIndent($node);
127
                }
128
                $previous = $target->add($node);
129
            }
130
            $this->attachBlankLines($previous);
131
            return Builder::buildContent($root, $this->_debug);
132
        } catch (\Error|\Exception|\ParseError $e) {
133
            $this->onError($e, $generator);
134
        }
135
    }
136
137
138
    /**
139
     * Attach blank(empty) Nodes savec in $blankBuffer to their parent (it means they are needed)
140
     *
141
     * @param array $emptyLines The empty lines
142
     * @param Node  $previous   The previous
143
     *
144
     * @return null
145
     */
146
    public function attachBlankLines(Node &$previous)
147
    {
148
        foreach ($this->_blankBuffer as $blankNode) {
149
            if ($blankNode !== $previous) {
150
                $blankNode->getParent()->add($blankNode);
151
            }
152
        }
153
        $this->_blankBuffer = [];
154
    }
155
156
    /**
157
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
158
     *
159
     * @param Node   $previous   The previous Node
160
     * @param array  $emptyLines The empty lines
161
     *
162
     * @return boolean  if True self::parse skips changing previous and adding to parent
163
     * @see self::parse
164
     */
165
    public function needsSpecialProcess(Node $current, Node &$previous):bool
166
    {
167
        $deepest = $previous->getDeepestNode();
168
        if ($deepest instanceof NodePartial) {
169
            return $deepest->specialProcess($current,  $this->_blankBuffer);
170
        } elseif(!($current instanceof NodePartial)) {
171
            return $current->specialProcess($previous, $this->_blankBuffer);
172
        }
173
        return false;
174
    }
175
176
    public function onError(object $e, $generator)
177
    {
178
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
179
        $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
180
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
181
            self::$error = $message;
182
            return null;
183
        }
184
        $line = $generator->key() ?? 'X';
185
        throw new \Exception($message." for $file:".$line, 1, $e);
186
    }
187
}
188