diff --git a/src/Storm/Model/Loader.php b/src/Storm/Model/Loader.php index 1a7a96607c64d280bd5468d33be2e551916374ec..0d7ba1ddf118726444a5cef3015dd232dacb3020 100644 --- a/src/Storm/Model/Loader.php +++ b/src/Storm/Model/Loader.php @@ -474,4 +474,77 @@ class Storm_Model_Loader { public function fetchAllBy($fields, $params) { return $this->getPersistenceStrategy()->fetchAllBy($fields, $params); } + + + public function clauseLike(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseLike($key, $value); + } + + + public function clauseNotLike(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseNotLike($key, $value); + } + + + public function clauseGreater(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseGreater($key, $value); + } + + + public function clauseGreaterEqual(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseGreaterEqual($key, $value); + } + + + public function clauseLesser(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseLesser($key, $value); + } + + + public function clauseLesserEqual(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseLesserEqual($key, $value); + } + + + public function clauseIs(string $key) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseIs($key); + } + + + public function clauseNotIs(string $key) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseNotIs($key); + } + + + public function clauseIn(string $key, array $array) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseIn($key, $array); + } + + + public function clauseNotIn(string $key, array $array) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseNotIn($key, $array); + } + + + public function clauseEqual(string $key, + string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseEqual($key, $value); + } + + + public function clauseNotEqual(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseNotEqual($key, $value); + } + + + public function clauseStart(string $key, + string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseStart($key, $value); + } + + + public function clauseEnd(string $key, + string $value) : Storm_Model_PersistenceStrategy_Clause { + return Storm_Model_PersistenceStrategy_Clause::clauseEnd($key, $value); + } } diff --git a/src/Storm/Model/PersistenceStrategy/Clause.php b/src/Storm/Model/PersistenceStrategy/Clause.php new file mode 100644 index 0000000000000000000000000000000000000000..b1db63ec7e60f8cde186de2f3f47de72bfce3f7d --- /dev/null +++ b/src/Storm/Model/PersistenceStrategy/Clause.php @@ -0,0 +1,234 @@ +<?php +/* +STORM is under the MIT License (MIT) + +Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +class Storm_Model_PersistenceStrategy_Clause { + + protected + $_key, + $_operator, + $_value, + $_array, + $_negated, + $_is_array; + + const + CLAUSE_WHERE = 'where', + CLAUSE_LIKE = 'like', + CLAUSE_EQUAL = '=', + CLAUSE_IN = 'in', + CLAUSE_IS = 'is', + CLAUSE_GREATER = '>', + CLAUSE_GREATER_EQUAL = '>=', + CLAUSE_LESSER = '<', + CLAUSE_LESSER_EQUAL = '<=', + PERCENT = '%'; + + + public function __construct(string $key, string $operator, $value_or_array) { + $this->_negated = false; + $this->_key = $key; + $this->_operator = $operator; + $this->_is_array = is_array($value_or_array); + + if ($this->_is_array) + $this->_array = $value_or_array; + + if (!$this->_is_array) + $this->_value = $value_or_array; + } + + + public static function clauseIs(string $key) : self { + return new static($key, static::CLAUSE_IS, null); + } + + + public static function clauseNotIs(string $key) : self { + return static::clauseIs($key)->_setNegated(true); + } + + + public static function clauseIn(string $key, array $array) : self { + return new static($key, static::CLAUSE_IN, $array); + } + + + public static function clauseNotIn(string $key, array $array) : self { + return static::clauseIn($key, $array)->_setNegated(true); + } + + + public static function clauseEqual(string $key, string $value) : self { + return new static($key, static::CLAUSE_EQUAL, $value); + } + + + public static function clauseNotEqual(string $key, string $value) : self { + return static::clauseEqual($key, $value)->_setNegated(true); + } + + + public static function clauseLike(string $key, string $value) : self { + return new static($key, static::CLAUSE_LIKE, $value); + } + + + public static function clauseNotLike(string $key, string $value) : self { + return static::clauseLike($key, $value)->_setNegated(true); + } + + + public static function clauseStart(string $key, string $value) : self { + return new static($key, static::CLAUSE_LIKE, $value . static::PERCENT); + } + + + public static function clauseEnd(string $key, string $value) : self { + return new static($key, static::CLAUSE_LIKE, static::PERCENT . $value); + } + + + public static function clauseGreater(string $key, string $value) : self { + return new static($key, static::CLAUSE_GREATER, $value); + } + + + public static function clauseGreaterEqual(string $key, string $value) : self { + return new static($key, static::CLAUSE_GREATER_EQUAL, $value); + } + + + public static function clauseLesser(string $key, string $value) : self { + return new static($key, static::CLAUSE_LESSER, $value); + } + + + public static function clauseLesserEqual(string $key, string $value) : self { + return new static($key, static::CLAUSE_LESSER_EQUAL, $value); + } + + + public static function newWith(string $key, $value_or_array) : self { + $operator = static::CLAUSE_EQUAL; + if (static::CLAUSE_WHERE === $key) + $operator = static::CLAUSE_WHERE; + + $is_like = ' like' === substr($key, -5); + $key = $is_like ? substr($key, 0, strlen($key) - 5) : $key; + $negated = (' not' === substr($key, -4)); + $key = $negated ? substr($key, 0, strlen($key) - 4) : $key; + + if ($is_like) + $operator = static::CLAUSE_LIKE; + + if (null === $value_or_array) + $operator = static::CLAUSE_IS; + + if (is_array($value_or_array)) + $operator = static::CLAUSE_IN; + + return (new static($key, $operator, $value_or_array))->_setNegated($negated); + } + + + protected function _setNegated(bool $negated) : self { + $this->_negated = $negated; + return $this; + } + + + protected function _getValueOrArray() { + return $this->_is_array + ? ($this->_array ?? []) + : ($this->_value ?? ''); + } + + + public function getFormatDb($table) : string { + if (static::CLAUSE_WHERE === $this->_operator) + return '(' . $this->_getValueOrArray() . ')'; + if (static::CLAUSE_IS === $this->_operator) + return $this->_key . ' ' . $this->_getOperator() . ' null'; + + return $table->getAdapter() + ->quoteInto($this->_clauseFormatDb(), + $this->_getValueOrArray(), null, null); + } + + + protected function _clauseFormatDb() : string { + if (static::CLAUSE_IN === $this->_operator) + return $this->_key . ' ' . $this->_getOperator() . ' (?)'; + if (static::CLAUSE_LIKE === $this->_operator) + return $this->_key . ' ' . $this->_getOperator() . ' ?'; + + return $this->_key . $this->_getOperator() . '?'; + } + + + protected function _getOperator() : string { + if (static::CLAUSE_EQUAL === $this->_operator) + return ($this->_negated ? '!' : '') . $this->_operator; + if (static::CLAUSE_IS === $this->_operator) + return $this->_operator . ($this->_negated ? ' not' : ''); + + return ($this->_negated ? 'not ' : '') . $this->_operator; + } + + + public function containAttibuteInVolatile($model) : bool { + if (!array_key_exists($this->_key, $model)) + return $this->_negated; + + if ($this->_is_array && 0 === count($this->_getValueOrArray())) + throw new Storm_Model_Exception(sprintf('array given for %s is empty', + $this->_key)); + + if ($this->_is_array && in_array($model[$this->_key], $this->_getValueOrArray())) + return !$this->_negated; + + if (static::CLAUSE_LIKE === $this->_operator) { + $matches = preg_match(('/^' . str_replace('%', '.*', + $this->_getValueOrArray()) . '$/i'), + $model[$this->_key]); + return $this->_negated ? !$matches : $matches; + } + + if (static::CLAUSE_GREATER === $this->_operator) + return $model[$this->_key] > $this->_getValueOrArray(); + + if (static::CLAUSE_GREATER_EQUAL === $this->_operator) + return $model[$this->_key] >= $this->_getValueOrArray(); + + if (static::CLAUSE_LESSER === $this->_operator) + return $model[$this->_key] < $this->_getValueOrArray(); + + if (static::CLAUSE_LESSER_EQUAL === $this->_operator) + return $model[$this->_key] <= $this->_getValueOrArray(); + + return ($this->_negated !== ($model[$this->_key] == $this->_getValueOrArray())); + } +} diff --git a/src/Storm/Model/PersistenceStrategy/Db.php b/src/Storm/Model/PersistenceStrategy/Db.php index 4dad81e9fda43345fd34a80246107cf7d3067e93..17890faef779dfac86b446126cfe228c0b9c5de6 100644 --- a/src/Storm/Model/PersistenceStrategy/Db.php +++ b/src/Storm/Model/PersistenceStrategy/Db.php @@ -25,7 +25,8 @@ THE SOFTWARE. */ -class Storm_Model_PersistenceStrategy_Db extends Storm_Model_PersistenceStrategy_Abstract { +class Storm_Model_PersistenceStrategy_Db + extends Storm_Model_PersistenceStrategy_Abstract { protected $_table; @@ -90,27 +91,17 @@ class Storm_Model_PersistenceStrategy_Db extends Storm_Model_PersistenceStrategy $where = []; foreach($where_or_clauses as $key => $value) $where []= $this->_generateWhereClauseForKeyAndValue($key, $value); + return implode(' and ', $where); } - protected function _generateWhereClauseForKeyAndValue(string $key, $value_or_array) : string { - if ('where' == $key) - return '(' . $value_or_array . ')'; - - if (' like' == substr($key, -5)) - return $this->quoteInto($key . ' ?', $value_or_array); - - $negated = (' not' == substr($key, -4)); - $key = $negated ? substr($key, 0, strlen($key) - 4) : $key; - - if (is_array($value_or_array)) - return $this->quoteInto($key . ($negated ? ' not': '') . ' in (?)', $value_or_array); + protected function _generateWhereClauseForKeyAndValue(string $key, $value) : string { + $clause = ($value instanceof Storm_Model_PersistenceStrategy_Clause) + ? $value + : Storm_Model_PersistenceStrategy_Clause::newWith($key, $value); - if (null === $value_or_array) - return $key . ' is' . ($negated ? ' not' : '') . ' null'; - - return $this->quoteInto($key . ($negated ? '!': '') . '=?', $value_or_array); + return $clause->getFormatDb($this->getTable()); } @@ -164,14 +155,17 @@ class Storm_Model_PersistenceStrategy_Db extends Storm_Model_PersistenceStrategy return $rows[0]['numberof']; } + public function insert($data) { return $this->getTable()->insert($data); } + public function lastInsertId() { return $this->getTable()->getAdapter()->lastInsertId(); } + /** * @see findAllBy */ @@ -192,12 +186,12 @@ class Storm_Model_PersistenceStrategy_Db extends Storm_Model_PersistenceStrategy $select = $this->getTable()->select(); foreach ($args as $field => $value) { - if (in_array($field, ['order', 'limit', 'where'])) { + if (in_array($field, ['order', 'limit', 'where'], true)) { $select->$field($value); continue; } - if (in_array($field, ['limitPage', 'scope'])) { + if (in_array($field, ['limitPage', 'scope'], true)) { $this->$field($select, $value); continue; } diff --git a/src/Storm/Model/PersistenceStrategy/Volatile.php b/src/Storm/Model/PersistenceStrategy/Volatile.php index b64668099eeba68d1fdea78ce36b03e7fd4277d2..0f160bac24089fd782244c924405588f55308922 100644 --- a/src/Storm/Model/PersistenceStrategy/Volatile.php +++ b/src/Storm/Model/PersistenceStrategy/Volatile.php @@ -193,45 +193,19 @@ class Storm_Model_PersistenceStrategy_Volatile extends Storm_Model_PersistenceS } - public function containsNoneAttributes(array $model, array $select) : bool { - foreach ($select as $key => $value) - if ($this->containsAttribute($model, $key, $value)) - return false; - - return true; - } - - public function containsAttribute(array $model, string $key, $value) : bool { - if (preg_match('/left\((.+),(.+)\)/', $key, $matches)) { - $key = trim($matches[1]); - $left_len = (int)$matches[2]; - } - - if (preg_match('/(.+)( not)? like/', $key, $matches)) { - $key = trim($matches[1]); - $like = '/^' . str_replace('%', '.*', $value) . '$/i'; - } - - if (!array_key_exists($key, $model)) - return false; - - if (is_array($value) && count($value) == 0) - throw new Storm_Model_Exception(sprintf('array given for %s is empty', - $key)); + $clause = ($value instanceof Storm_Model_PersistenceStrategy_Clause) + ? $value + : null; - if (is_array($value) && in_array($model[$key], $value)) - return true; + if (!$clause && preg_match('/left\((.+),(.+)\)/', $key, $matches)) + $clause = Storm_Model_PersistenceStrategy_Clause::clauseLike(trim($matches[1]), + (substr($value, 0, (int)$matches[2]) . '%')); - if (isset($left_len) - && is_string($value) - && substr($model[$key], 0, $left_len) === substr($value, 0, $left_len)) - return true; + if (!$clause) + $clause = Storm_Model_PersistenceStrategy_Clause::newWith($key, $value); - if (isset($like)) - return preg_match($like, $model[$key]); - - return $model[$key] == $value; + return $clause->containAttibuteInVolatile($model); } @@ -301,28 +275,17 @@ class Storm_Model_PersistenceStrategy_Volatile extends Storm_Model_PersistenceS protected function _allMatchingInstancesDo(array $clauses, callable $callback) : int { - $negations = []; - - foreach($clauses as $k => $v) { - if (false !== strpos($k, ' not')) { - $negations[str_replace(' not', '', $k)] = $v; - unset($clauses[$k]); - } - } - $matching_instances = array_filter($this->getInstancesArray(), - function($model) use ($clauses, $negations) { - return $this->_modelMatchesClausesAndNegations($model, $clauses, $negations); + function ($model) use ($clauses) { + return $this->_modelMatchesClauses($model, $clauses); }); return count(array_map($callback, $matching_instances)); } - protected function _modelMatchesClausesAndNegations(array $model, array $clauses, array $negations) : bool { - return - $this->containsAllAttributes($model, $clauses) - && (empty($negations) || $this->containsNoneAttributes($model, $negations)); + protected function _modelMatchesClauses(array $model, array $clauses) : bool { + return $this->containsAllAttributes($model, $clauses); } @@ -335,5 +298,3 @@ class Storm_Model_PersistenceStrategy_Volatile extends Storm_Model_PersistenceS return true; } } - -?> diff --git a/tests/Storm/Test/LoaderTest.php b/tests/Storm/Test/LoaderTest.php index 30b515301f1691c7f04a0023b5e4512b371dc877..29650d4721337b214f9a786e63b3807cf20fea40 100644 --- a/tests/Storm/Test/LoaderTest.php +++ b/tests/Storm/Test/LoaderTest.php @@ -157,11 +157,42 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase { public function findAllByGeneratedSelects() { return [ - [ ['name not' => 'Harlock'] , ['name!=\'Harlock\''] ], - [ ['name not' => ['Harlock', 'Nausicaa']] , ['name not in (\'Harlock\',\'Nausicaa\')'] ], - [ ['name not' => null] , ['name is not null'] ], - [ ['login like' => '%aus%'] , ['login like \'%aus%\''] ], - [ ['login not like' => '%aus%'] , ['login not like \'%aus%\''] ] + [ ['name' => null] , ['name is null'] ], + [ ['name' => ['Harlock', 'Nausicaa']], ['name in (\'Harlock\',\'Nausicaa\')'] ], + [ ['name not' => 'Harlock'], ['name!=\'Harlock\''] ], + [ ['name not' => ['Harlock', 'Nausicaa']], + ['name not in (\'Harlock\',\'Nausicaa\')'] ], + [ ['name not' => null], ['name is not null'] ], + [ ['login like' => '%aus%'], ['login like \'%aus%\''] ], + [ ['login not like' => '%aus%'], ['login not like \'%aus%\''] ], + [ ['where' => 'id=\'29\' and name=\'lp\''], ['id=\'29\' and name=\'lp\''] ], + [ ['left(name, 5)' => 'ganda'], ['left(name, 5)=\'ganda\''] ], + [ ['id' => '29'], ['id=\'29\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseGreater('id', '30') ], + ['id>\'30\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseGreaterEqual('id', '30') ], + ['id>=\'30\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseLesser('id', '30') ], + ['id<\'30\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseLesserEqual('id', '30') ], + ['id<=\'30\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseIs('name') ], ['name is null'] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseIn('name', ['Harlock', 'Nausicaa']) ], + ['name in (\'Harlock\',\'Nausicaa\')'] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseNotEqual('name', 'Harlock') ], + ['name!=\'Harlock\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseNotIn('name', ['Harlock', 'Nausicaa']) ], + ['name not in (\'Harlock\',\'Nausicaa\')'] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseLike('login', '%aus%') ], + ['login like \'%aus%\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseNotLike('login', '%aus%') ], + ['login not like \'%aus%\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseEqual('left(name, 5)', 'ganda') ], + ['left(name, 5)=\'ganda\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseStart('login', 'aus') ], + ['login like \'aus%\''] ], + [ [ Storm_Model_PersistenceStrategy_Clause::clauseEnd('login', 'aus') ], + ['login like \'%aus\''] ], ]; } diff --git a/tests/Storm/Test/LoaderVolatileTest.php b/tests/Storm/Test/LoaderVolatileTest.php index a42e61b95d74cdfa94b3df2aa6b80d5a840ba103..87e553715454546c7df8dc3282981ce28778b627 100644 --- a/tests/Storm/Test/LoaderVolatileTest.php +++ b/tests/Storm/Test/LoaderVolatileTest.php @@ -179,7 +179,7 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase { /** @test */ public function changingAlbertBrainShouldUpdateItsReferenceInBrainId() { $better_brain = $this->fixture(Storm_Test_VolatileBrain::class, ['id' => 3, - 'weight' => 150]); + 'weight' => 150]); $this->albert->setBrain($better_brain); $this->assertEquals(3, $this->albert->getBrainId()); } @@ -777,5 +777,76 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase { ->collect('login') ->getArrayCopy()); } + + + /** @test */ + public function selectAllCatsWhereIdMoreThanThreeShouldAnswersFifiAndLoulou() { + $this->assertEquals(['fifi', 'loulou'], + (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseGreater('id', 3)]))) + ->collect('name') + ->getArrayCopy()); + } + + + /** @test */ + public function selectAllCatsWhereIdMoreOrEqualThanForShouldAnswersFifiAndLoulou() { + $this->assertEquals(['fifi', 'loulou'], + (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseGreaterEqual('id', 4)]))) + ->collect('name') + ->getArrayCopy()); + } + + + /** @test */ + public function selectAllCatsWhereIdLesserThanFiveShouldAnswersRiriAndFifi() { + $this->assertEquals(['riri', 'fifi'], + (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseLesser('id', 5)]))) + ->collect('name') + ->getArrayCopy()); + } + + + /** @test */ + public function selectAllCatsWhereIdLesserOrEqualThanForShouldAnswersRiriAndFifi() { + $this->assertEquals(['riri', 'fifi'], + (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseLesserEqual('id', 4)]))) + ->collect('name') + ->getArrayCopy()); + } + + + /** @test */ + public function selectUserLoginNotHubertShouldReturnAlbertAndZoe() { + $this->assertEquals(['albert', 'zoe'], + (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseNotEqual('login', 'hubert')]))) + ->collect('login') + ->getArrayCopy()); + } + + + /** @test */ + public function selectUserLoginInAlbertZoeAndFooNotLikeSnafuShouldReturnAlbert() { + $this->assertEquals(['albert'], + (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseIn('login', ['albert', 'zoe']), Storm_Test_VolatileUser::clauseNotLike('foo', '%naf%')]))) + ->collect('login') + ->getArrayCopy()); + } + + + /** @test */ + public function selectUserLoginInAlbertZoeAndFooStartSnaShouldReturnZoe() { + $this->assertEquals(['zoe'], + (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseIn('login', ['albert', 'zoe']), Storm_Test_VolatileUser::clauseStart('foo', 'sna')]))) + ->collect('login') + ->getArrayCopy()); + } + + + /** @test */ + public function selectUserLoginInAlbertZoeAndFooEndAfuShouldReturnZoe() { + $this->assertEquals(['zoe'], + (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseIn('login', ['albert', 'zoe']), Storm_Test_VolatileUser::clauseEnd('foo', 'afu')]))) + ->collect('login') + ->getArrayCopy()); + } } -?>