FieldPath::parse()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 25
ccs 14
cts 14
cp 1
rs 9.4888
cc 5
nc 5
nop 1
crap 5
1
<?php
2
3
namespace Bdf\Form\Util;
4
5
use Bdf\Form\Aggregate\ChildAggregateInterface;
6
use Bdf\Form\Child\ChildInterface;
7
use Bdf\Form\ElementInterface;
8
9
/**
10
 * Represents a path for resolve a field
11
 * This path can be absolute (start from the root element) or relative (start from the current element)
12
 *
13
 * @see FieldPath::parse() To create the path from a string
14
 */
15
final class FieldPath
16
{
17
    const SELF_ELEMENT = '.';
18
    const PARENT_ELEMENT = '..';
19
    const SEPARATOR = '/';
20
21
    /**
22
     * Paths cache
23
     *
24
     * @var FieldPath[]
25
     */
26
    private static $cache = [];
27
28
    /**
29
     * @var string[]
30
     */
31
    private $path;
32
33
    /**
34
     * @var bool
35
     */
36
    private $absolute;
37
38
39
    /**
40
     * FieldPath constructor.
41
     *
42
     * @param string[] $path The path
43
     * @param bool $absolute true to start the resolution from the root element
44
     *
45
     * @see FieldPath::parse() Prefer use this method instead of the constructor
46
     */
47 8
    public function __construct(array $path, bool $absolute)
48
    {
49 8
        $this->path = $path;
50 8
        $this->absolute = $absolute;
51 8
    }
52
53
    /**
54
     * Resolve the element using the path from the current element
55
     *
56
     * @param ElementInterface|ChildInterface $currentElement The base element
57
     *
58
     * @return ElementInterface|null The resolved element, or null if cannot be found
59
     */
60 15
    public function resolve($currentElement): ?ElementInterface
61
    {
62 15
        if ($currentElement instanceof ChildInterface) {
63 1
            $currentElement = $currentElement->element();
64
        }
65
66 15
        if ($this->absolute) {
67 4
            $currentElement = $currentElement->root();
68
        }
69
70 15
        foreach ($this->path as $part) {
71 15
            if ($part === self::PARENT_ELEMENT) {
72 13
                if (($container = $currentElement->container()) === null) {
73 1
                    return null;
74
                }
75
76 13
                $currentElement = $container->parent();
77 13
                continue;
78
            }
79
80 15
            if (!$currentElement instanceof ChildAggregateInterface || !isset($currentElement[$part])) {
81 2
                return null;
82
            }
83
84 15
            $currentElement = $currentElement[$part]->element();
85
        }
86
87 15
        return $currentElement;
88
    }
89
90
    /**
91
     * Resolve sibling element value
92
     *
93
     * Usage:
94
     * <code>
95
     * $builder->string('password');
96
     * $builder->string('confirmation')->depends('password')->satisfy(function ($value, $input) {
97
     *     if ($value !== FieldPath::resolve('password')->value($input)) {
98
     *         return 'Invalid';
99
     *     }
100
     * })
101
     * </code>
102
     *
103
     * @param ElementInterface|ChildInterface $currentElement The base element
104
     *
105
     * @return mixed|null The element value, or null if not found
106
     */
107 12
    public function value($currentElement)
108
    {
109 12
        if (!$element = $this->resolve($currentElement)) {
110 1
            return null;
111
        }
112
113 12
        return $element->value();
114
    }
115
116
    /**
117
     * Parse a field path
118
     *
119
     * The format is :
120
     * [.|..|/] [fieldName] [/fieldName]...
121
     *
122
     * With :
123
     * - "." to start the path from the current element (and not from it's parent). The current element must be an aggregate element like a form to works
124
     * - ".." to start the path from the parent of the current element. This is the default behavior, so it's not necessary to start with "../" the path
125
     * - "/" is the fields separator. When used at the beginning of the path it means that the path is absolute (i.e. start from the root element)
126
     * - "fieldName" is a field name. The name is the declared one, not the HTTP field name
127
     *
128
     * Example:
129
     * FieldPath::parse("firstName") : Access to the sibling field named "firstName"
130
     * FieldPath::parse("../firstName") : Same as above
131
     * FieldPath::parse(".") : References the current element
132
     * FieldPath::parse("person/firstName") : Get the field "firstName" under the sibling embedded form "person"
133
     * FieldPath::parse("/foo/bar") : Get the field "bar" under "foo", starting from the root element
134
     *
135
     * @param string $path Path as string
136
     *
137
     * @return self
138
     */
139 16
    public static function parse(string $path): self
140
    {
141 16
        if (isset(self::$cache[$path])) {
142 16
            return self::$cache[$path];
143
        }
144
145 8
        if ($path[0] === self::SEPARATOR) {
146 3
            return self::$cache[$path] = new self(explode(self::SEPARATOR, substr($path, 1)), true);
147
        }
148
149 8
        $parts = explode(self::SEPARATOR, $path);
150
151 8
        switch ($parts[0]) {
152 8
            case self::SELF_ELEMENT:
153 2
                array_shift($parts);
154 2
                break;
155
156 7
            case self::PARENT_ELEMENT:
157 3
                break;
158
159
            default:
160 6
                array_unshift($parts, self::PARENT_ELEMENT);
161
        }
162
163 8
        return self::$cache[$path] = new self($parts, false);
164
    }
165
}
166