Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 835a2b7

Browse files
committed
Add lazy models for BSON documents and arrays
1 parent 9ce2cbf commit 835a2b7

File tree

5 files changed

+1064
-1
lines changed

5 files changed

+1064
-1
lines changed

‎psalm-baseline.xml‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
</MixedArrayAccess>
7070
</file>
7171
<file src="src/Model/AsListIterator.php">
72-
<InvalidTemplateParamoccurrences="1">
72+
<InvalidTemplateParam>
7373
<code>AsListIterator</code>
7474
</InvalidTemplateParam>
7575
</file>
@@ -173,6 +173,66 @@
173173
<code><![CDATA[$this->index['name']]]></code>
174174
</MixedReturnStatement>
175175
</file>
176+
<file src="src/Model/LazyBSONArray.php">
177+
<MixedArgument>
178+
<code>$offset</code>
179+
<code>$offset</code>
180+
<code>$offset</code>
181+
</MixedArgument>
182+
<MixedArgumentTypeCoercion>
183+
<code><![CDATA[new CallbackFilterIterator(
184+
$itemIterator,
185+
/** @param TValue $value */
186+
function ($value, int $offset) use (&$seen): bool {
187+
return ! isset($this->unset[$offset]) && ! isset($seen[$offset]);
188+
},
189+
)]]></code>
190+
<code><![CDATA[new CallbackIterator(
191+
// Skip keys that were unset or handled in a previous iterator
192+
new CallbackFilterIterator(
193+
$itemIterator,
194+
/** @param TValue $value */
195+
function ($value, int $offset) use (&$seen): bool {
196+
return ! isset($this->unset[$offset]) && ! isset($seen[$offset]);
197+
},
198+
),
199+
/**
200+
* @param TValue $value
201+
* @return TValue
202+
*/
203+
function ($value, int $offset) use (&$seen) {
204+
// Mark key as seen, skipping any future occurrences
205+
$seen[$offset] = true;
206+
207+
// Return actual value (potentially overridden by offsetSet)
208+
return $this->offsetGet($offset);
209+
},
210+
)]]></code>
211+
</MixedArgumentTypeCoercion>
212+
<MixedArrayAssignment>
213+
<code>$seen[$offset]</code>
214+
</MixedArrayAssignment>
215+
<RedundantConditionGivenDocblockType>
216+
<code>is_array($input)</code>
217+
</RedundantConditionGivenDocblockType>
218+
<RedundantFunctionCallGivenDocblockType>
219+
<code>array_values</code>
220+
</RedundantFunctionCallGivenDocblockType>
221+
</file>
222+
<file src="src/Model/LazyBSONDocument.php">
223+
<MismatchingDocblockReturnType>
224+
<code><![CDATA[Iterator<string, TValue>]]></code>
225+
</MismatchingDocblockReturnType>
226+
<MixedAssignment>
227+
<code>$value</code>
228+
</MixedAssignment>
229+
<MixedInferredReturnType>
230+
<code><![CDATA[Iterator<string, TValue>]]></code>
231+
</MixedInferredReturnType>
232+
<RedundantConditionGivenDocblockType>
233+
<code>is_object($input)</code>
234+
</RedundantConditionGivenDocblockType>
235+
</file>
176236
<file src="src/Operation/Aggregate.php">
177237
<MixedArgument>
178238
<code><![CDATA[$this->options['typeMap']]]></code>

‎src/Model/LazyBSONArray.php‎

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<?php
2+
/*
3+
* Copyright 2023-present MongoDB, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace MongoDB\Model;
19+
20+
use AppendIterator;
21+
use ArrayAccess;
22+
use ArrayIterator;
23+
use CallbackFilterIterator;
24+
use IteratorAggregate;
25+
use MongoDB\BSON\PackedArray;
26+
use MongoDB\Exception\InvalidArgumentException;
27+
use ReturnTypeWillChange;
28+
29+
use function array_key_exists;
30+
use function array_keys;
31+
use function array_map;
32+
use function array_merge;
33+
use function array_values;
34+
use function is_array;
35+
use function is_numeric;
36+
use function max;
37+
use function MongoDB\recursive_copy;
38+
use function sprintf;
39+
use function trigger_error;
40+
41+
use const E_USER_WARNING;
42+
43+
/**
44+
* Model class for a BSON array.
45+
*
46+
* The internal data will be filtered through array_values() during BSON
47+
* serialization to ensure that it becomes a BSON array.
48+
*
49+
* @template TValue
50+
* @template-implements ArrayAccess<int, TValue>
51+
* @template-implements IteratorAggregate<int, TValue>
52+
*/
53+
class LazyBSONArray implements ArrayAccess, IteratorAggregate
54+
{
55+
/** @var PackedArray<TValue> */
56+
private PackedArray $bson;
57+
58+
/** @var array<int, TValue> */
59+
private array $read = [];
60+
61+
/** @var array<int, bool> */
62+
private array $exists = [];
63+
64+
/** @var array<int, TValue> */
65+
private array $set = [];
66+
67+
/** @var array<int, true> */
68+
private array $unset = [];
69+
70+
private bool $entirePackedArrayRead = false;
71+
72+
/**
73+
* Deep clone this lazy array.
74+
*/
75+
public function __clone()
76+
{
77+
$this->bson = clone $this->bson;
78+
79+
foreach ($this->set as $key => $value) {
80+
$this->set[$key] = recursive_copy($value);
81+
}
82+
}
83+
84+
/**
85+
* Constructs a lazy BSON array.
86+
*
87+
* @param PackedArray<TValue>|list<TValue>|null $input An input for a lazy array.
88+
* When given a BSON array, this is treated as input. For lists
89+
* this constructs a new BSON array using fromPHP.
90+
*/
91+
public function __construct($input = null)
92+
{
93+
if ($input === null) {
94+
$this->bson = PackedArray::fromPHP([]);
95+
} elseif ($input instanceof PackedArray) {
96+
$this->bson = $input;
97+
} elseif (is_array($input)) {
98+
$this->bson = PackedArray::fromPHP([]);
99+
$this->set = array_values($input);
100+
$this->exists = array_map(
101+
/** @param TValue $value */
102+
function ($value): bool {
103+
return true;
104+
},
105+
$this->set,
106+
);
107+
} else {
108+
throw InvalidArgumentException::invalidType('input', $input, [PackedArray::class, 'array', 'null']);
109+
}
110+
}
111+
112+
/** @return AsListIterator<TValue> */
113+
public function getIterator(): AsListIterator
114+
{
115+
$itemIterator = new AppendIterator();
116+
// Iterate through all fields in the BSON array
117+
$itemIterator->append($this->bson->getIterator());
118+
// Then iterate over all fields that were set
119+
$itemIterator->append(new ArrayIterator($this->set));
120+
121+
/** @var array<int, bool> $seen */
122+
$seen = [];
123+
124+
// Use AsListIterator to ensure we're indexing from 0 without gaps
125+
return new AsListIterator(
126+
new CallbackIterator(
127+
// Skip keys that were unset or handled in a previous iterator
128+
new CallbackFilterIterator(
129+
$itemIterator,
130+
/** @param TValue $value */
131+
function ($value, int $offset) use (&$seen): bool {
132+
return ! isset($this->unset[$offset]) && ! isset($seen[$offset]);
133+
},
134+
),
135+
/**
136+
* @param TValue $value
137+
* @return TValue
138+
*/
139+
function ($value, int $offset) use (&$seen) {
140+
// Mark key as seen, skipping any future occurrences
141+
$seen[$offset] = true;
142+
143+
// Return actual value (potentially overridden by offsetSet)
144+
return $this->offsetGet($offset);
145+
},
146+
),
147+
);
148+
}
149+
150+
/** @param mixed $offset */
151+
public function offsetExists($offset): bool
152+
{
153+
if (! is_numeric($offset)) {
154+
return false;
155+
}
156+
157+
$offset = (int) $offset;
158+
159+
// If we've looked for the value, return the cached result
160+
if (isset($this->exists[$offset])) {
161+
return $this->exists[$offset];
162+
}
163+
164+
return $this->exists[$offset] = $this->bson->has($offset);
165+
}
166+
167+
/**
168+
* @param mixed $offset
169+
* @return TValue
170+
*/
171+
#[ReturnTypeWillChange]
172+
public function offsetGet($offset)
173+
{
174+
if (! is_numeric($offset)) {
175+
trigger_error(sprintf('Undefined offset: %s', $offset), E_USER_WARNING);
176+
177+
return null;
178+
}
179+
180+
$offset = (int) $offset;
181+
$this->readFromBson($offset);
182+
183+
if (isset($this->unset[$offset]) || ! $this->exists[$offset]) {
184+
trigger_error(sprintf('Undefined offset: %d', $offset), E_USER_WARNING);
185+
186+
return null;
187+
}
188+
189+
return array_key_exists($offset, $this->set) ? $this->set[$offset] : $this->read[$offset];
190+
}
191+
192+
/**
193+
* @param mixed $offset
194+
* @param TValue $value
195+
*/
196+
public function offsetSet($offset, $value): void
197+
{
198+
if ($offset === null) {
199+
$this->readEntirePackedArray();
200+
201+
$existingItems = array_merge(
202+
array_keys($this->read),
203+
array_keys($this->set),
204+
);
205+
206+
$offset = $existingItems === [] ? 0 : max($existingItems) + 1;
207+
} elseif (! is_numeric($offset)) {
208+
trigger_error(sprintf('Unsupported offset: %s', $offset), E_USER_WARNING);
209+
210+
return;
211+
} else {
212+
$offset = (int) $offset;
213+
}
214+
215+
$this->set[$offset] = $value;
216+
unset($this->unset[$offset]);
217+
$this->exists[$offset] = true;
218+
}
219+
220+
/** @param mixed $offset */
221+
public function offsetUnset($offset): void
222+
{
223+
if (! is_numeric($offset)) {
224+
trigger_error(sprintf('Undefined offset: %s', $offset), E_USER_WARNING);
225+
226+
return;
227+
}
228+
229+
$offset = (int) $offset;
230+
$this->unset[$offset] = true;
231+
$this->exists[$offset] = false;
232+
unset($this->set[$offset]);
233+
}
234+
235+
private function readEntirePackedArray(): void
236+
{
237+
if ($this->entirePackedArrayRead) {
238+
return;
239+
}
240+
241+
foreach ($this->bson as $offset => $value) {
242+
$this->read[$offset] = $value;
243+
244+
if (! isset($this->exists[$offset])) {
245+
$this->exists[$offset] = true;
246+
}
247+
}
248+
249+
$this->entirePackedArrayRead = true;
250+
}
251+
252+
private function readFromBson(int $offset): void
253+
{
254+
if (array_key_exists($offset, $this->read)) {
255+
return;
256+
}
257+
258+
// Read value if it's present in the BSON structure
259+
$found = false;
260+
if ($this->bson->has($offset)) {
261+
$found = true;
262+
$this->read[$offset] = $this->bson->get($offset);
263+
}
264+
265+
// Mark the offset as "existing" if it wasn't previously marked already
266+
if (! isset($this->exists[$offset])) {
267+
$this->exists[$offset] = $found;
268+
}
269+
}
270+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /