Passed
Push — master ( 3b6a73...d590ba )
by stéphane
02:55
created

Loader::getSourceGenerator()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.2944

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 16
ccs 9
cts 11
cp 0.8182
rs 8.8333
c 0
b 0
f 0
cc 7
nc 7
nop 1
crap 7.2944
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 11
    public function __construct($absolutePath = null, $options = null, $debug = 0)
49
    {
50 11
        $this->_debug   = is_null($debug) ? 0 : min($debug, 3);
51 11
        $this->_options = is_int($options) ? $options : $this->_options;
52 11
        if (is_string($absolutePath)) {
53 1
            $this->load($absolutePath);
54
        }
55 11
    }
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 1
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
70
        }
71 2
        $this->filePath = $absolutePath;
72 2
        $adle = "auto_detect_line_endings";
73 2
        $prevADLE = ini_get($adle);
74 2
        !$prevADLE && ini_set($adle, "true");
75 2
        $content = @file($absolutePath, FILE_IGNORE_NEW_LINES);
76 2
        !$prevADLE && ini_set($adle, "false");
77 2
        if (is_bool($content)) {
78 1
            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 3
    private function getSourceGenerator($strContent = null):\Generator
93
    {
94 3
        if (is_null($strContent) && is_null($this->content)) {
95 1
            throw new \Exception(self::EXCEPTION_LINE_SPLIT);
96
        }
97 2
        if (!is_null($this->content)) {
98
            $source = $this->content;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->content can also be of type boolean. However, the property $content is declared as type array|false|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
99
        } else { //TODO : be more permissive on $strContent values
100 2
            $simplerLineFeeds = preg_replace('/(\r\n|\r)/', "\n", $strContent);
101 2
            $source = preg_split("/\n/m", $simplerLineFeeds, 0, PREG_SPLIT_DELIM_CAPTURE);
102
        }
103 2
        if (!is_array($source) || !count($source)) {
104
            throw new \Exception(self::EXCEPTION_LINE_SPLIT);
105
        }
106 2
        foreach ($source as $key => $value) {
107 2
            yield ++$key => $value;
108
        }
109 2
    }
110
111
    /**
112
     * Parse Yaml lines into a hierarchy of Node
113
     *
114
     * @param string $strContent The Yaml string or null to parse loaded content
115
     *
116
     * @throws \Exception    if content is not available as $strContent or as $this->content (from file)
117
     * @throws \ParseError  if any error during parsing or building
118
     *
119
     * @return array|YamlObject|null      null on errors if NO_PARSING_EXCEPTIONS is set, otherwise an array of YamlObject or just YamlObject
120
     */
121 2
    public function parse($strContent = null)
122
    {
123 2
        $generator = $this->getSourceGenerator($strContent);
124 2
        $previous = $root = new NodeRoot();
125
        try {
126 2
            foreach ($generator as $lineNb => $lineString) {
127 2
                $node = NodeFactory::get($lineString, $lineNb);
128 2
                if ($this->_debug === 1) echo get_class($node)."\n";
129 2
                if ($this->needsSpecialProcess($node, $previous)) continue;
130 2
                $this->attachBlankLines($previous);
131 2
                switch ($node->indent <=> $previous->indent) {
132 1
                    case -1: $target = $previous->getTargetOnLessIndent($node);
133 1
                        break;
134 2
                    case 0:  $target = $previous->getTargetOnEqualIndent($node);
135 1
                        break;
136 2
                    default: $target = $previous->getTargetOnMoreIndent($node);
137
                }
138 2
                $previous = $target->add($node);
139
            }
140 2
            $this->attachBlankLines($previous);
141 2
            if ($this->_debug === 1){
142
                return;
143
            }
144 2
            return Builder::buildContent($root, $this->_debug);
145 1
        } catch (\Error|\Exception|\ParseError $e) {
146 1
            $this->onError($e, $generator);
147
        }
148
    }
149
150
151
    /**
152
     * Attach blank(empty) Nodes savec in $blankBuffer to their parent (it means they are needed)
153
     *
154
     * @param array $emptyLines The empty lines
155
     * @param Node  $previous   The previous
156
     *
157
     * @return null
158
     */
159 3
    public function attachBlankLines(Node &$previous)
160
    {
161 3
        foreach ($this->_blankBuffer as $blankNode) {
162 1
            if ($blankNode !== $previous) {
163 1
                $blankNode->getParent()->add($blankNode);
164
            }
165
        }
166 3
        $this->_blankBuffer = [];
167 3
    }
168
169
    /**
170
     * For certain (special) Nodes types some actions are required BEFORE parent assignment
171
     *
172
     * @param Node   $previous   The previous Node
173
     * @param array  $emptyLines The empty lines
174
     *
175
     * @return boolean  if True self::parse skips changing previous and adding to parent
176
     * @see self::parse
177
     */
178 3
    public function needsSpecialProcess(Node $current, Node &$previous):bool
179
    {
180 3
        $deepest = $previous->getDeepestNode();
181 3
        if ($deepest instanceof NodePartial) {
182 1
            return $deepest->specialProcess($current,  $this->_blankBuffer);
183 3
        } elseif(!($current instanceof NodePartial)) {
184 3
            return $current->specialProcess($previous, $this->_blankBuffer);
185
        }
186 1
        return false;
187
    }
188
189 3
    public function onError(object $e, \Generator $generator)
190
    {
191 3
        $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
192 3
        $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
193 3
        if ($this->_options & self::NO_PARSING_EXCEPTIONS) {
194 1
            self::$error = $message;
195 1
            return null;
196
        }
197 2
        $line = $generator->key() ?? 'X';
198 2
        throw new \Exception($message." for $file:".$line, 1, $e);
199
    }
200
}
201