Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Fields
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
4 / 4
9
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hydrate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 form
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 validate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\User;
6
7use Celema\Sire\Result;
8use Cosray\Context;
9use Cosray\Exception\RuntimeException;
10use Cosray\Field\Capability\Translatable;
11use Cosray\Field\Definitions;
12use Cosray\Field\Field;
13use Cosray\Field\FieldHydrator;
14use Cosray\Field\Fieldsets;
15use Cosray\Field\Services;
16use Cosray\Node\FieldOwner;
17use Cosray\User;
18use Cosray\Validation\Shapes;
19
20/**
21 * The field-typed part of a user model: the properties a user class declares
22 * beyond the account, stored under `content` in `users.data`.
23 */
24final class Fields
25{
26    private readonly FieldHydrator $hydrator;
27
28    public function __construct(
29        Services $services,
30        private readonly Context $context,
31    ) {
32        $this->hydrator = new FieldHydrator($services);
33    }
34
35    /** @return array<string, Field> */
36    public function hydrate(User $user): array
37    {
38        $this->hydrator->hydrate($user, $user->content(), new FieldOwner($this->context, $user->uid));
39        $fields = FieldHydrator::getFields($user, Definitions::for($user::class)->names());
40
41        foreach ($fields as $name => $field) {
42            if ($field instanceof Translatable && $field->isTranslatable()) {
43                $class = $user::class;
44                throw new RuntimeException("User field '{$class}::\${$name}' must not be translated.");
45            }
46        }
47
48        return $fields;
49    }
50
51    /**
52     * What the panel's field views render, in the shape the node editor
53     * hands them.
54     *
55     * @return array{fields: list<array<mixed>>, fieldsets: list<array<mixed>>, content: array<string, array<mixed>>}
56     */
57    public function form(User $user): array
58    {
59        $fields = $this->hydrate($user);
60        $stored = $user->content();
61        $content = [];
62
63        foreach ($fields as $name => $field) {
64            $structure = $field->structure();
65            $content[$name] = array_merge($structure, $stored[$name] ?? []);
66            $content[$name]['type'] = $structure['type'];
67        }
68
69        return [
70            'fields' => array_values(array_map(static fn(Field $field): array => $field->properties(), $fields)),
71            'fieldsets' => Fieldsets::serialize(
72                Definitions::for($user::class)->fieldsets(),
73                array_keys($fields),
74                $fields,
75                $user::class,
76            ),
77            'content' => $content,
78        ];
79    }
80
81    /** @param array<string, mixed> $content */
82    public function validate(User $user, array $content): Result
83    {
84        $shape = Shapes::create();
85
86        foreach ($this->hydrate($user) as $name => $field) {
87            $shape
88                ->add($name, $field->shape())
89                ->label($field->getLabel() ?? $name)
90                ->optional()
91                ->nullable();
92        }
93
94        $form = Shapes::create();
95        $form->add('content', $shape);
96
97        return $form->validate(['content' => $content]);
98    }
99}