Skip to content

Commit 06f9433

Browse files
committed
integrated parameterhelper
1 parent b7a3762 commit 06f9433

File tree

7 files changed

+232
-24
lines changed

7 files changed

+232
-24
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ RUN apt-get update && apt-get install -y \
1919
ARG WITH_XDEBUG=false
2020

2121
RUN if [ $WITH_XDEBUG = "true" ] ; then \
22-
pecl install channel://pecl.php.net/xdebug-3.0.1; \
22+
pecl install channel://pecl.php.net/xdebug-2.9.3; \
2323
docker-php-ext-enable xdebug; \
2424
fi;
2525
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,21 @@ $tsx->rollback();
127127
$tsx->commit([Statement::create('MATCH (x) RETURN x LIMIT 100')]);
128128
```
129129

130+
### Differentiating between parameter type
131+
132+
Cypher has lists and maps. This notion can be problematic as the standard php arrays encapsulate both. When you provide an empty array as a parameter, it will be impossible to determine if it is an empty list or map.
133+
134+
The `ParameterHelper` class is the ideal companion for this:
135+
136+
```php
137+
use Laudis\Neo4j\ParameterHelper;
138+
139+
$client->run('MATCH (x) WHERE x.slug in $listOrMap RETURN x', ['listOrMap' => ParameterHelper::asList([])]); // will return an empty set
140+
$client->run('MATCH (x) WHERE x.slug in $listOrMap RETURN x', ['listOrMap' => ParameterHelper::asMap([])]); // will error
141+
$client->run('MATCH (x) WHERE x.slug in $listOrMap RETURN x', ['listOrMap' => []]); // will error
142+
```
143+
144+
This helper can also be used to make intent explicit.
130145

131146
### Providing custom injections
132147

@@ -163,9 +178,9 @@ Flexibility is maintained where possible by making all parameters iterables if t
163178

164179
```php
165180
// Vanilla flavour
166-
$client->run('MATCH (x {slug: $slug})', ['slug' => 'a']);
181+
use Ds\Map;$client->run('MATCH (x {slug: $slug})', ['slug' => 'a']);
167182
// php-ds implementation
168-
$client->run('MATCH (x {slug: $slug})', new \Ds\Map(['slug' => 'a']));
183+
$client->run('MATCH (x {slug: $slug})', new Map(['slug' => 'a']));
169184
// laravel style
170185
$client->run('MATCH (x {slug: $slug})', collect(['slug' => 'a']));
171186
```

src/Databags/Statement.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,19 @@
1313

1414
namespace Laudis\Neo4j\Databags;
1515

16-
use Ds\Map;
17-
1816
final class Statement
1917
{
2018
private string $text;
21-
/** @var array<string, scalar|iterable|null> */
22-
private array $parameters;
19+
/** @var iterable<string, scalar|iterable|null> */
20+
private iterable $parameters;
2321

2422
/**
2523
* @param iterable<string, scalar|iterable|null> $parameters
2624
*/
2725
public function __construct(string $text, iterable $parameters)
2826
{
2927
$this->text = $text;
30-
$this->parameters = (new Map($parameters))->toArray();
28+
$this->parameters = $parameters;
3129
}
3230

3331
/**
@@ -44,9 +42,9 @@ public function getText(): string
4442
}
4543

4644
/**
47-
* @return array<string, scalar|iterable|null>
45+
* @return iterable<string, scalar|iterable|null>
4846
*/
49-
public function getParameters(): array
47+
public function getParameters(): iterable
5048
{
5149
return $this->parameters;
5250
}

src/Formatter/HttpCypherFormatter.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Laudis\Neo4j\Databags\RequestData;
2222
use Laudis\Neo4j\Databags\Statement;
2323
use Laudis\Neo4j\Databags\StatementStatistics;
24+
use Laudis\Neo4j\ParameterHelper;
2425
use stdClass;
2526

2627
/**
@@ -151,8 +152,8 @@ public function prepareBody(iterable $statements, RequestData $config): string
151152
'resultDataContents' => ['ROW'],
152153
'includeStats' => $config->includeStats(),
153154
];
154-
$parameters = $statement->getParameters();
155-
$st['parameters'] = $parameters === [] ? new stdClass() : $parameters;
155+
$parameters = ParameterHelper::formatParameters($statement->getParameters());
156+
$st['parameters'] = $parameters->count() === 0 ? new stdClass() : $parameters->toArray();
156157
$tbr[] = $st;
157158
}
158159

src/Network/Bolt/BoltSession.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Laudis\Neo4j\Exception\Neo4jException;
2727
use Laudis\Neo4j\Formatter\BoltCypherFormatter;
2828
use Laudis\Neo4j\HttpDriver\Transaction;
29+
use Laudis\Neo4j\ParameterHelper;
2930
use Throwable;
3031

3132
final class BoltSession implements SessionInterface
@@ -117,20 +118,19 @@ public function openTransaction(iterable $statements = null): TransactionInterfa
117118
*/
118119
private function runStatements(iterable $statements, Bolt $bolt): Vector
119120
{
120-
try {
121-
$tbr = new Vector();
122-
foreach ($statements as $statement) {
123-
$extra = ['db' => $this->injections->database()];
124-
/** @var array $parameters */
125-
$parameters = $statement->getParameters();
121+
$tbr = new Vector();
122+
foreach ($statements as $statement) {
123+
$extra = ['db' => $this->injections->database()];
124+
$parameters = ParameterHelper::formatParameters($statement->getParameters());
125+
try {
126126
/** @var array{fields: array<int, string>} $meta */
127-
$meta = $bolt->run($statement->getText(), $parameters, $extra);
127+
$meta = $bolt->run($statement->getText(), $parameters->toArray(), $extra);
128128
/** @var array<array> $results */
129129
$results = $bolt->pullAll();
130-
$tbr->push($this->formatter->formatResult($meta, $results));
130+
} catch (Throwable $e) {
131+
throw new Neo4jException(new Vector([new Neo4jError('', $e->getMessage())]), $e);
131132
}
132-
} catch (Throwable $e) {
133-
throw new Neo4jException(new Vector([new Neo4jError('', $e->getMessage())]), $e);
133+
$tbr->push($this->formatter->formatResult($meta, $results));
134134
}
135135

136136
return $tbr;

src/ParameterHelper.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Laudis Neo4j package.
7+
*
8+
* (c) Laudis technologies <http://laudis.tech>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Laudis\Neo4j;
15+
16+
use Ds\Map;
17+
use Ds\Sequence;
18+
use Ds\Vector;
19+
use InvalidArgumentException;
20+
use stdClass;
21+
22+
final class ParameterHelper
23+
{
24+
public static function asList(iterable $iterable): Sequence
25+
{
26+
return new Vector($iterable);
27+
}
28+
29+
public static function asMap(iterable $iterable): Map
30+
{
31+
return new Map($iterable);
32+
}
33+
34+
/**
35+
* @param iterable|scalar|null $value
36+
*
37+
* @return iterable|scalar|stdClass|null
38+
*/
39+
public static function asParameter($value)
40+
{
41+
if ($value instanceof Sequence && $value->count() === 0) {
42+
return [];
43+
}
44+
if (($value instanceof Map && $value->count() === 0) ||
45+
(is_array($value) && count($value) === 0)
46+
) {
47+
return new stdClass();
48+
}
49+
if (is_iterable($value)) {
50+
return self::iterableToArray($value);
51+
}
52+
53+
return $value;
54+
}
55+
56+
/**
57+
* @param iterable<iterable|scalar|null> $parameters
58+
*
59+
* @return Map<array-key, iterable|scalar|stdClass|null>
60+
*/
61+
public static function formatParameters(iterable $parameters): iterable
62+
{
63+
/** @var Map<array-key, iterable|scalar|stdClass|null> $tbr */
64+
$tbr = new Map();
65+
foreach ($parameters as $key => $value) {
66+
if (!(is_int($key) || is_string($key))) {
67+
$msg = 'The parameters must have an integer or string as key values, '.gettype($key).' received.';
68+
throw new InvalidArgumentException($msg);
69+
}
70+
$tbr->put($key, self::asParameter($value));
71+
}
72+
73+
return $tbr;
74+
}
75+
76+
private static function iterableToArray(iterable $value): array
77+
{
78+
$tbr = [];
79+
/**
80+
* @var mixed $key
81+
* @var mixed $val
82+
*/
83+
foreach ($value as $key => $val) {
84+
if (is_int($key) || is_string($key)) {
85+
/** @psalm-suppress MixedAssignment */
86+
$tbr[$key] = $val;
87+
} else {
88+
$msg = 'Iterable parameters must have an integer or string as key values, '.gettype($key).' received.';
89+
throw new InvalidArgumentException($msg);
90+
}
91+
}
92+
93+
return $tbr;
94+
}
95+
}

tests/Integration/ComplexQueryTests.php

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313

1414
namespace Laudis\Neo4j\Tests\Integration;
1515

16+
use Generator;
17+
use InvalidArgumentException;
1618
use Laudis\Neo4j\ClientBuilder;
1719
use Laudis\Neo4j\Contracts\ClientInterface;
20+
use Laudis\Neo4j\Exception\Neo4jException;
21+
use Laudis\Neo4j\ParameterHelper;
1822
use PHPUnit\Framework\TestCase;
1923

2024
final class ComplexQueryTests extends TestCase
@@ -25,11 +29,106 @@ protected function setUp(): void
2529
{
2630
parent::setUp();
2731
$this->client = ClientBuilder::create()
28-
->addBoltConnection('bolt', 'bolt://neo4j:test@neo4j')
29-
->addHttpConnection('http', 'http://neo4j:test@neo4j')
32+
->addBoltConnection('bolt', 'bolt://neo4j:test@neo4j-42')
33+
->addHttpConnection('http', 'http://neo4j:test@neo4j-42')
3034
->build();
3135
}
3236

37+
/**
38+
* @dataProvider transactionProvider
39+
*/
40+
public function testListParameterHelper(string $alias): void
41+
{
42+
$result = $this->client->run(<<<'CYPHER'
43+
MATCH (x) WHERE x.slug in $listOrMap RETURN x
44+
CYPHER, ['listOrMap' => ParameterHelper::asList([])], $alias);
45+
self::assertEquals(0, $result->count());
46+
}
47+
48+
/**
49+
* @dataProvider transactionProvider
50+
*/
51+
public function testValidListParameterHelper(string $alias): void
52+
{
53+
$result = $this->client->run(<<<'CYPHER'
54+
RETURN $listOrMap AS x
55+
CYPHER, ['listOrMap' => ParameterHelper::asList([1, 2, 3])], $alias);
56+
self::assertEquals(1, $result->count());
57+
self::assertEquals([1, 2, 3], $result->first()->get('x'));
58+
}
59+
60+
/**
61+
* @dataProvider transactionProvider
62+
*/
63+
public function testValidMapParameterHelper(string $alias): void
64+
{
65+
$result = $this->client->run(<<<'CYPHER'
66+
RETURN $listOrMap AS x
67+
CYPHER, ['listOrMap' => ParameterHelper::asMap(['a' => 'b', 'c' => 'd'])], $alias);
68+
self::assertEquals(1, $result->count());
69+
self::assertEquals(['a' => 'b', 'c' => 'd'], $result->first()->get('x'));
70+
}
71+
72+
/**
73+
* @dataProvider transactionProvider
74+
*/
75+
public function testMapParameterHelper(string $alias): void
76+
{
77+
$this->expectException(Neo4jException::class);
78+
79+
$this->client->run(<<<'CYPHER'
80+
MERGE (x:Node {slug: 'a'})
81+
WITH x
82+
MATCH (x) WHERE x.slug in $listOrMap RETURN x
83+
CYPHER, ['listOrMap' => ParameterHelper::asMap(['a' => 'b'])], $alias);
84+
}
85+
86+
/**
87+
* @dataProvider transactionProvider
88+
*/
89+
public function testArrayParameterHelper(string $alias): void
90+
{
91+
$this->expectException(Neo4jException::class);
92+
$this->client->run(<<<'CYPHER'
93+
MERGE (x:Node {slug: 'a'})
94+
WITH x
95+
MATCH (x) WHERE x.slug in $listOrMap RETURN x
96+
CYPHER, ['listOrMap' => []], $alias);
97+
}
98+
99+
/**
100+
* @dataProvider transactionProvider
101+
*/
102+
public function testInvalidParameter(string $alias): void
103+
{
104+
$this->expectException(InvalidArgumentException::class);
105+
$this->client->run(<<<'CYPHER'
106+
MERGE (x:Node {slug: 'a'})
107+
WITH x
108+
MATCH (x) WHERE x.slug in $listOrMap RETURN x
109+
CYPHER, ['listOrMap' => self::generate()], $alias);
110+
}
111+
112+
private static function generate(): Generator
113+
{
114+
foreach (range(1, 3) as $x) {
115+
yield true => $x;
116+
}
117+
}
118+
119+
/**
120+
* @dataProvider transactionProvider
121+
*/
122+
public function testInvalidParameters(string $alias): void
123+
{
124+
$this->expectException(InvalidArgumentException::class);
125+
$this->client->run(<<<'CYPHER'
126+
MERGE (x:Node {slug: 'a'})
127+
WITH x
128+
MATCH (x) WHERE x.slug in $listOrMap RETURN x
129+
CYPHER, self::generate(), $alias);
130+
}
131+
33132
/**
34133
* @dataProvider transactionProvider
35134
*/

0 commit comments

Comments
 (0)