Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Rebuild
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
3 / 3
13
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
 run
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 node
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Fulltext;
6
7use Celema\Container\Container;
8use Celema\Quma\Database;
9use Cosray\Bootstrap;
10use Cosray\Exception\RuntimeException;
11use Cosray\Field\Services;
12use Cosray\Locales;
13use Cosray\Util\Transaction;
14use Throwable;
15
16final class Rebuild
17{
18    private readonly Sync $sync;
19
20    public function __construct(
21        private readonly Database $db,
22        private readonly Locales $locales,
23        Services $services,
24        private readonly Container $container,
25    ) {
26        $this->sync = new Sync($db, new Builder($services->types, $services->blocks));
27    }
28
29    /**
30     * @param null|callable(string, Throwable): void $failure
31     * @return array{processed: int, indexed: int, empty: int, failed: int, missingTitles: int}
32     */
33    public function run(?callable $failure = null): array
34    {
35        $this->sync->validate($this->locales);
36        $classes = [];
37        $tag = $this->container->tag(Bootstrap::NODE_TAG);
38        foreach ($tag->entries() as $handle) {
39            $classes[$handle] = $tag->entry($handle)->definition();
40        }
41
42        $report = ['processed' => 0, 'indexed' => 0, 'empty' => 0, 'failed' => 0, 'missingTitles' => 0];
43        $ceiling = (int) $this->db->fulltext->ceiling()->one()['node'];
44        $after = 0;
45        while ($keys = $this->db->fulltext->keys(['after' => $after, 'ceiling' => $ceiling])->all()) {
46            foreach ($keys as $key) {
47                $after = (int) $key['node'];
48                $report['processed']++;
49
50                try {
51                    $result = Transaction::run($this->db, fn(): array => $this->node($after, $classes));
52                    $report[$result['indexed'] > 0 ? 'indexed' : 'empty']++;
53                    $report['missingTitles'] += $result['missingTitles'];
54                } catch (Throwable $e) {
55                    $report['failed']++;
56                    if ($failure !== null) {
57                        $failure($key['uid'], $e);
58                    }
59                }
60            }
61        }
62        return $report;
63    }
64
65    /**
66     * @param array<mixed> $classes
67     * @return array{indexed: int, missingTitles: int}
68     */
69    private function node(int $node, array $classes): array
70    {
71        // Read after acquiring the same row lock taken by a live UPDATE.
72        $row = $this->db->fulltext->lock(['node' => $node])->first();
73        if ($row === null || $row['deleted'] !== null) {
74            $this->db->fulltext->delete(['node' => $node])->run();
75            return ['indexed' => 0, 'missingTitles' => 0];
76        }
77
78        $class = $classes[$row['handle']] ?? null;
79        if (!is_string($class) || !class_exists($class)) {
80            throw new RuntimeException("No configured schema for type '{$row['handle']}'.");
81        }
82
83        return $this->sync->replace(
84            $node,
85            $row['uid'],
86            $class,
87            json_decode($row['content'], true, flags: JSON_THROW_ON_ERROR),
88            json_decode($row['title'], true, flags: JSON_THROW_ON_ERROR),
89            $this->locales,
90        );
91    }
92}