Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Writer
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
4 / 4
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 draft
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 assertUsablePaths
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Node;
6
7use Cosray\Actor;
8use Cosray\Cms;
9use Cosray\Context;
10use Cosray\Exception\RuntimeException;
11
12/**
13 * Creates CMS nodes without exposing blueprint, serialization, and storage wiring.
14 *
15 * @api
16 */
17final class Writer
18{
19    private readonly Factory $factory;
20    private readonly Serializer $serializer;
21    private readonly Store $store;
22
23    public function __construct(
24        private readonly Context $context,
25        private readonly Cms $cms,
26        Types $types,
27    ) {
28        $this->factory = $cms->nodeFactory();
29        $this->serializer = new Serializer($types, $this->factory->uid());
30        $this->store = new Store(
31            $context->db,
32            new PathManager(),
33            $types,
34            $this->factory->uid(),
35            factory: $this->factory,
36            cms: $cms,
37            context: $context,
38        );
39    }
40
41    /**
42     * @param class-string $class
43     * @param array<string, mixed> $values
44     */
45    public function draft(string $class, array $values = []): Draft
46    {
47        $node = $this->factory->blueprint($class, $this->context, $this->cms);
48        $data = $this->serializer->blueprint(
49            $node,
50            Factory::fieldNamesFor($node),
51            $this->context->locales(),
52            $values,
53        );
54
55        return new Draft($node, $data);
56    }
57
58    /** @return array{success: true, uid: string} */
59    public function create(Draft $draft, ?Actor $actor = null): array
60    {
61        $this->assertUsablePaths($draft->data());
62
63        return $this->store->create(
64            $draft->node,
65            $draft->data(),
66            $this->context->locales(),
67            $actor ?? Actor::system(),
68        );
69    }
70
71    /**
72     * Explicit paths fail loudly here: `PathManager` would silently
73     * suffix a colliding path, which defeats preserving a legacy URL.
74     */
75    private function assertUsablePaths(array $data): void
76    {
77        $locales = $this->context->locales();
78
79        foreach ($data['paths'] ?? [] as $locale => $path) {
80            if (!$path) {
81                continue;
82            }
83
84            if (!$locales->exists($locale)) {
85                throw new RuntimeException(
86                    "Unknown locale '{$locale}' for the node path '{$path}'",
87                );
88            }
89
90            if ($this->context->db->nodes->activePathExists(['path' => $path])->first()) {
91                throw new RuntimeException("The URL path '{$path}' is already in use");
92            }
93        }
94    }
95}