Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
HasOneOf
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
5 / 5
27
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChild
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getEmptyRelationshipValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEagerRelationships
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
21
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Db\Record\Relationships;
16
17use Pop\Db\Record;
18use Pop\Db\Sql\Parser;
19
20/**
21 * Relationship class for "has one of" relationships
22 *
23 * @category   Pop
24 * @package    Pop\Db
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    7.0.0
29 */
30class HasOneOf extends AbstractRelationship
31{
32
33    /**
34     * Parent record
35     * @var Record
36     */
37    protected ?Record $parent = null;
38
39    /**
40     * Constructor
41     *
42     * Instantiate the relationship object
43     *
44     * @param Record $parent
45     * @param string $foreignTable
46     * @param string|array $foreignKey
47     * @param ?array $options
48     */
49    public function __construct(Record $parent, string $foreignTable, string|array $foreignKey, ?array $options = null)
50    {
51        parent::__construct($foreignTable, $foreignKey, $options);
52        $this->parent = $parent;
53    }
54
55    /**
56     * Get parent record
57     *
58     * @return ?Record
59     */
60    public function getParent(): ?Record
61    {
62        return $this->parent;
63    }
64
65    /**
66     * Get child
67     *
68     * @return Record
69     */
70    public function getChild(): Record
71    {
72        $table = $this->foreignTable;
73
74        $this->assertKeyCardinality($this->foreignKey, (new $table())->getPrimaryKeys());
75
76        $id = is_array($this->foreignKey) ?
77            array_map(fn($col) => $this->parent[$col], $this->foreignKey) : $this->parent[$this->foreignKey];
78
79        if (!empty($this->children)) {
80            return $table::with($this->children)->getById($id);
81        } else {
82            return $table::findById($id);
83        }
84    }
85
86    /**
87     * Get the value to use when no eager-loaded result exists for a given leaf record
88     *
89     * @return mixed
90     */
91    public function getEmptyRelationshipValue(): mixed
92    {
93        return null;
94    }
95
96    /**
97     * Get eager relationships
98     *
99     * @param  array $ids
100     * @throws Exception
101     * @return array
102     */
103    public function getEagerRelationships(array $ids): array
104    {
105        if (($this->foreignTable === null) || ($this->foreignKey === null)) {
106            throw new Exception('Error: The foreign table and key values have not been set.');
107        }
108
109        $results = [];
110        $table   = $this->foreignTable;
111        $db      = $table::db();
112        $sql     = $db->createSql();
113        $columns = null;
114
115        if (!empty($this->options)) {
116            if (isset($this->options['select'])) {
117                $columns = $this->options['select'];
118            }
119        }
120
121        $keys = (new $table())->getPrimaryKeys();
122        $this->assertKeyCardinality($this->foreignKey, $keys);
123
124        $sql->select($columns)->from($table::table());
125
126        if (count($keys) == 1) {
127            $keys = reset($keys);
128        }
129
130        $params = [];
131
132        $this->applyEagerIdFilter($sql, $keys, $ids, $params);
133
134        if (!empty($this->options)) {
135            if (isset($this->options['limit'])) {
136                $sql->select()->limit((int)$this->options['limit']);
137            }
138
139            if (isset($this->options['offset'])) {
140                $sql->select()->offset((int)$this->options['offset']);
141            }
142            if (isset($this->options['join'])) {
143                $joins = (is_array($this->options['join']) && isset($this->options['join']['table'])) ?
144                    [$this->options['join']] : $this->options['join'];
145
146                foreach ($joins as $join) {
147                    if (isset($join['type']) && method_exists($sql->select(), $join['type'])) {
148                        $joinMethod = $join['type'];
149                        $sql->select()->{$joinMethod}($join['table'], $join['columns']);
150                    } else {
151                        $sql->select()->leftJoin($join['table'], $join['columns']);
152                    }
153                }
154            }
155            if (isset($this->options['order'])) {
156                if (!is_array($this->options['order'])) {
157                    $orders = (str_contains($this->options['order'], ',')) ?
158                        explode(',', $this->options['order']) : [$this->options['order']];
159                } else {
160                    $orders = $this->options['order'];
161                }
162                foreach ($orders as $order) {
163                    $ord = Parser\Order::parse(trim($order));
164                    $sql->select()->orderBy($ord['by'], $db->escape($ord['order']));
165                }
166            }
167        }
168
169        $db->prepare($sql)
170           ->bindParams($params)
171           ->execute();
172
173        $rows        = $db->fetchAll();
174        $results     = [];
175        $leafRecords = [];
176
177        foreach ($rows as $row) {
178            $record = new $table();
179            $record->setColumns($row);
180            $resultKey = is_array($keys) ?
181                self::buildCompositeKey(array_map(fn($col) => $row[$col], $keys)) : $row[$keys];
182            $results[$resultKey] = $record;
183            $leafRecords[] = $record;
184        }
185
186        // $keys already holds the foreign table's own primary key columns (a plain string
187        // when there is only one), which is what nested child relationships look their
188        // leaf records up by — NOT this relationship's foreign key columns, which name
189        // columns on the declaring side.
190        $this->hydrateChildRelationships($leafRecords, $keys);
191
192        return $results;
193    }
194
195}