Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.81% covered (warning)
69.81%
37 / 53
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Wrapper
69.81% covered (warning)
69.81%
37 / 53
50.00% covered (danger)
50.00%
5 / 10
62.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 path
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 unwrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 meta
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 label
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 children
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 __get
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
13.12
 __isset
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
10.50
 __call
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Node;
6
7use Cosray\Cms;
8use Cosray\Context;
9use Cosray\Exception\RuntimeException;
10use Cosray\Finder\Nodes;
11use Cosray\Locale;
12use Cosray\Title\Resolver as TitleResolver;
13use ReflectionClass;
14
15class Wrapper
16{
17    public readonly Meta $meta;
18
19    public function __construct(
20        private readonly object $node,
21        private readonly array $fieldNames,
22        private readonly Types $types,
23        private readonly ?Context $context = null,
24        private readonly ?Cms $cms = null,
25        private readonly ?Factory $nodeFactory = null,
26    ) {
27        $this->meta = new Meta($this->node, $this->types);
28    }
29
30    /**
31     * Resolve the locale-aware URL path for this node.
32     *
33     * Uses the paths stored in Factory's WeakMap data,
34     * walking the locale fallback chain until a path is found.
35     */
36    public function path(?Locale $locale = null): string
37    {
38        $data = Factory::dataFor($this->node);
39        $paths = $data['paths'] ?? [];
40
41        if (!$locale && $this->context) {
42            $locale = $this->context->locale();
43        }
44
45        while ($locale) {
46            if (isset($paths[$locale->id])) {
47                return $paths[$locale->id];
48            }
49
50            $locale = $locale->fallback();
51        }
52
53        throw new RuntimeException('No url path found');
54    }
55
56    /**
57     * Return the inner node if the given object is a Wrapper,
58     * otherwise return the object unchanged.
59     */
60    public static function unwrap(object $object): object
61    {
62        return $object instanceof self ? $object->node : $object;
63    }
64
65    public function meta(string $key, mixed $default = null): mixed
66    {
67        return $this->meta->get($key, $default);
68    }
69
70    public function title(): string
71    {
72        return new TitleResolver($this->types)->resolve(self::unwrap($this->node));
73    }
74
75    /**
76     * The node's title for listings, pickers and other labelling, read from the
77     * materialized title column instead of resolving it again per node.
78     *
79     * Falls back to title() when the stored map has nothing for the active
80     * locale chain: a node saved before the column existed, or a locale added
81     * since the last `db:titles` rebuild.
82     *
83     * A dynamic title composed from *other* nodes can be stale here â€” the map
84     * is rewritten when the node itself is saved, not when its sources change.
85     * Use title() where that matters; rendering still resolves live.
86     */
87    public function label(?Locale $locale = null): string
88    {
89        $map = Factory::dataFor($this->node)['title'] ?? null;
90
91        if (!is_array($map)) {
92            return $this->title();
93        }
94
95        if (!$locale && $this->context) {
96            $locale = $this->context->locale();
97        }
98
99        return new TitleResolver($this->types)->stored($map, $locale) ?? $this->title();
100    }
101
102    public function children(string $query = ''): Nodes
103    {
104        if ($this->context === null || $this->cms === null || $this->nodeFactory === null) {
105            // Finders and the view renderer pass the cms and context, so every
106            // wrapper reaching a template or a node can do this. Only the
107            // title-resolution proxy in Serializer is built without them.
108            throw new RuntimeException('This node proxy was built without cms access');
109        }
110
111        $children = new Nodes($this->context, $this->cms, $this->nodeFactory, $this->types)
112            ->published(null)
113            ->hidden(null)
114            ->childrenOf($this->meta->uid);
115
116        if (trim($query) !== '') {
117            $children->filter($query);
118        }
119
120        return $children;
121    }
122
123    public function __get(string $name): mixed
124    {
125        if (in_array($name, $this->fieldNames, true)) {
126            $value = Factory::fieldFor($this->node, $name)->value();
127
128            return $value->isset() ? $value : null;
129        }
130
131        $embedded = Factory::embeddedFor($this->node, $name);
132
133        if ($embedded !== null) {
134            return $embedded;
135        }
136
137        if (!property_exists($this->node, $name)) {
138            return null;
139        }
140
141        $property = new ReflectionClass($this->node)->getProperty($name);
142
143        return $property->isPublic() && $property->isInitialized($this->node)
144            ? $property->getValue($this->node)
145            : null;
146    }
147
148    public function __isset(string $name): bool
149    {
150        if (in_array($name, $this->fieldNames, true)) {
151            return Factory::fieldFor($this->node, $name)->value()->isset();
152        }
153
154        if (Factory::embeddedFor($this->node, $name) !== null) {
155            return true;
156        }
157
158        if (!property_exists($this->node, $name)) {
159            return false;
160        }
161
162        $property = new ReflectionClass($this->node)->getProperty($name);
163
164        return (
165            $property->isPublic()
166                && $property->isInitialized($this->node)
167                && $property->getValue($this->node) !== null
168        );
169    }
170
171    public function __call(string $name, array $args): mixed
172    {
173        return $this->node->{$name}(...$args);
174    }
175}