Completed
Push — master ( c45521...844759 )
by stéphane
08:22
created

Loader::load()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 9
nop 1
dl 0
loc 16
rs 9.5555
c 0
b 0
f 0
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
    //privates
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
33
    //Exceptions messages
34
    private const INVALID_VALUE        = self::class.": at line %d";
35
    private const EXCEPTION_NO_FILE    = self::class.": file '%s' does not exists (or path is incorrect?)";
36
    private const EXCEPTION_READ_ERROR = self::class.": file '%s' failed to be loaded (permission denied ?)";
37
    private const EXCEPTION_LINE_SPLIT = self::class.": content is not a string(maybe a file error?)";
38
39
    /**
40
     * Loader constructor
41
     *
42
     * @param string|null       $absolutePath The absolute file path
43
     * @param int|null          $options      The options (bitmask as int value)
44
     * @param integer|bool|null $debug        The debug level as either boolean (true=1) or any integer
45
     */
46
    public function __construct($absolutePath = null, $options = null, $debug = 0)
47
    {
48
        $this->debug   = is_int($debug) ? min($debug, 3) : 1;
49
        $this->options = is_int($options) ? $options : $this->options;
50
        if (is_string($absolutePath)) {
51
            $this->load($absolutePath);
52
        }
53
    }
54
55
    /**
56
     * Load a file and save its content as $content
57
     *
58
     * @param string $absolutePath The absolute path of a file
59
     *
60
     * @throws \Exception if file don't exist OR reading failed
61
     *
62
     * @return self  ( returns the same Loader  )
63
     */
64
    public function load(string $absolutePath):Loader
65
    {
66
        if (!file_exists($absolutePath)) {
67
            throw new \Exception(sprintf(self::EXCEPTION_NO_FILE, $absolutePath));
68
        }
69
        $this->filePath = $absolutePath;
70
        $adle = "auto_detect_line_endings";
71
        $prevADLE = ini_get($adle);
72
        !$prevADLE && ini_set($adle, "true");
73
        $content = file($absolutePath, FILE_IGNORE_NEW_LINES);
74
        !$prevADLE && ini_set($adle, "false");
75
        if (is_bool($content)) {
76
            throw new \Exception(sprintf(self::EXCEPTION_READ_ERROR, $absolutePath));
77
        }
78
        $this->content = $content;
79
        return $this;
80
    }
81
82
    /**
83
     * Gets the source iterator.
84
     *
85
     * @param string|null $strContent  The string content
86
     *
87
     * @throws \Exception if self::content is empty or splitting on linefeed has failed
88
     * @return \Closure  The source iterator.
89
     */
90
    private function getSourceIterator($strContent = null):\Closure
91
    {
92
        $source = $this->content ?? preg_split("/\n/m", preg_replace('/(\r\n|\r)/', "\n", $strContent), 0, PREG_SPLIT_DELIM_CAPTURE);
93
        //TODO : be more permissive on $strContent values
94
        if (!is_array($source) || !count($source)) throw new \Exception(self::EXCEPTION_LINE_SPLIT);
95
        return function () use($source) {
96
            foreach ($source as $key => $value) {
97
                yield ++$key => $value;
98
            }
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
        $sourceIterator = $this->getSourceIterator($strContent)();
115
        $previous = $root = new NodeRoot();
116
        $emptyLines = [];
117
        $currentLine = 0;
118
        try {
119
            foreach ($sourceIterator as $lineNb => $lineString) {
120
                $currentLine = $lineNb;
121
                $node = NodeFactory::get($lineString, $lineNb);
122
                if ($node->needsSpecialProcess($previous, $emptyLines)) continue;
123
                $this->attachBlankLines($emptyLines, $previous);
124
                $emptyLines = [];
125
                switch ($node->indent <=> $previous->indent) {
126
                    case -1: $target = $node->getTargetOnLessIndent($previous);
127
                        break;
128
                    case 0:  $target = $node->getTargetOnEqualIndent($previous);
129
                        break;
130
                    default: $target = $node->getTargetOnMoreIndent($previous);
131
                }
132
                if ($node->skipOnContext($target)) continue;//var_dump(get_class($target));
133
                $target->add($node);
134
                $previous = $node;
135
            }
136
            if ($this->debug === 2) print_r($root);
137
            return Builder::buildContent($root, $this->debug);
138
        } catch (\Error|\Exception|\ParseError $e) {
139
            $file = $this->filePath ? realpath($this->filePath) : '#YAML STRING#';
140
            $message = $e->getMessage()."\n ".$e->getFile().":".$e->getLine();
141
            if ($this->options & self::NO_PARSING_EXCEPTIONS) {
142
                self::$error = $message;
143
                return null;
144
            }
145
            throw new \Exception($message." for $file:$currentLine", 1, $e);
146
        }
147
    }
148
149
150
    /**
151
     * Attach blank(empty) Nodes savec in $emptylines to their parent (it means they are needed)
152
     *
153
     * @param array $emptyLines The empty lines
154
     * @param Node  $previous   The previous
155
     *
156
     * @return null
157
     */
158
    public function attachBlankLines(array &$emptyLines, Node &$previous)
159
    {
160
        foreach ($emptyLines as $blankNode) {
161
            if ($blankNode !== $previous) {
162
                $blankNode->getParent()->add($blankNode);
163
            }
164
        }
165
    }
166
167
}
168