Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement remaining conformance tests for Firestore. #1207

Merged
merged 2 commits into from
Aug 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions Firestore/phpunit-conformance.xml.dist

This file was deleted.

12 changes: 11 additions & 1 deletion Firestore/src/DocumentReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ public function set(array $fields, array $options = [])
* as a dot-delimited string (i.e. `foo.bar`), or an instance of
* {@see Google\Cloud\Firestore\FieldPath}. Nested arrays are not allowed.
*
* Please note that conflicting paths will result in an exception. Paths
* conflict when one path indicates a location nested within another path.
* For instance, path `a.b` cannot be set directly if path `a` is also
* provided.
*
* Example:
* ```
* $document->update([
Expand Down Expand Up @@ -281,9 +286,14 @@ public function set(array $fields, array $options = [])
* @codingStandardsIgnoreStart
* @see https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.Firestore.Commit Commit
*
* @param array[] $data A list of arrays of form `[FieldPath|string $path, mixed $value]`.
* @param array[] $data A list of arrays of form
* `[FieldPath|string $path, mixed $value]`.
* @param array $options Configuration options
* @return array [WriteResult](https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.WriteResult)
* @throws \InvalidArgumentException If data is given in an invalid format
* or is empty.
* @throws \InvalidArgumentException If any field paths are empty.
* @throws \InvalidArgumentException If field paths conflict.
* @codingStandardsIgnoreEnd
*/
public function update(array $data, array $options = [])
Expand Down
135 changes: 130 additions & 5 deletions Firestore/src/FieldPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,103 @@
*/
class FieldPath
{
const SPECIAL_CHARS = '/^[^*~\/[\]]+$/';
const UNESCAPED_FIELD_NAME = '/^[_a-zA-Z][_a-zA-Z0-9]*$/';

/**
* @var array
*/
private $fieldNames;

/**
* @param array $fieldNames A list of field names.
* @throws \InvalidArgumentException If an empty path element is provided.
*/
public function __construct(array $fieldNames)
{
foreach ($fieldNames as $field) {
// falsey is good enough unless the field is actually `0`.
if (!$field && $field !== 0) {
throw new \InvalidArgumentException(sprintf(
'Field paths cannot contain empty path elements. Given path was `%s`.',
implode('.', $fieldNames)
));
}
}

$this->fieldNames = $fieldNames;
}

/**
* Create a FieldPath from a string path.
*
* Example:
* ```
* use Google\Cloud\Firestore\FieldPath;
*
* $path = FieldPath::fromString('path.to.field');
* ```
*
* @param string $path The field path string.
* @param bool $splitPath If false, the input path will not be split on `.`.
* **Defaults to** `true`.
* @return FieldPath
* @throws \InvalidArgumentException If an invalid path is provided.
*/
public static function fromString($path, $splitPath = true)
{
self::validateString($path);

$parts = $splitPath
? explode('.', $path)
: [$path];

return new self($parts);
}

/**
* Get a new FieldPath with the given path part appended to the current
* path.
*
* Example:
* ```
* $child = $path->child('element');
* ```
*
* @param string $part The child path part.
* @return FieldPath
* @throws \InvalidArgumentException If an empty path element is provided.
*/
public function child($part)
{
$fieldNames = $this->fieldNames;
$fieldNames[] = $part;

return new static($fieldNames);
}

/**
* Get the current path as a string, with special characters escaped.
*
* Example:
* ```
* $string = $path->pathString();
* ```
*
* @return string
*/
public function pathString()
{
$out = [];
foreach ($this->fieldNames as $part) {
$out[] = $this->escapePathPart($part);
}

$fieldPath = implode('.', $out);

return $fieldPath;
}

/**
* Get the path elements.
*
Expand All @@ -52,14 +139,52 @@ public function path()
}

/**
* Create a FieldPath from a string path.
* Cast the path to a string.
*
* @param string $path
* @return FieldPath
* @return string
* @access private
*/
public static function fromString($path)
public function __toString()
{
return new self(explode('.', $path));
return $this->pathString();
}

/**
* Test a field path component, checking for any special characters,
* and escaping as required.
*
* @param string $part The raw field path component.
* @return string
*/
private function escapePathPart($part)
{
// If no special characters are found, return the input unchanged.
if (preg_match(self::UNESCAPED_FIELD_NAME, $part)) {
return $part;
}

// If the string is already wrapped in backticks, return as-is.
if (substr($part, 0, 1) === '`' && substr($part, -1) === '`' && strlen($part) > 1) {
return $part;
}

return '`' . str_replace('`', '\\`', str_replace('\\', '\\\\', $part)) . '`';
}

/**
* Check if a given string field path is valid.
*
* @param string $fieldPath
* @throws \InvalidArgumentException
*/
private static function validateString($fieldPath)
{
if (strpos($fieldPath, '..')) {
throw new \InvalidArgumentException('Paths cannot contain `..`.');
}

if (strpos($fieldPath, '.') === 0 || strpos(strrev($fieldPath), '.') === 0) {
throw new \InvalidArgumentException('Paths cannot begin or end with `.`.');
}
}
}
15 changes: 13 additions & 2 deletions Firestore/src/FieldValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,20 @@ public static function serverTimestamp()
*/
public static function isSentinelValue($value)
{
return in_array($value, [
return in_array($value, self::sentinelValues(), true);
}

/**
* Get a list of sentinel values.
*
* @return array
* @access private
*/
public static function sentinelValues()
{
return [
self::deleteField(),
self::serverTimestamp()
]);
];
}
}
26 changes: 20 additions & 6 deletions Firestore/src/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ class Query
'<=' => self::OP_LESS_THAN_OR_EQUAL,
'>' => self::OP_GREATER_THAN,
'>=' => self::OP_GREATER_THAN_OR_EQUAL,
'=' => self::OP_EQUAL
'=' => self::OP_EQUAL,
'==' => self::OP_EQUAL,
'===' => self::OP_EQUAL,
];

private $allowedDirections = [
Expand Down Expand Up @@ -236,8 +238,12 @@ public function select(array $fieldPaths)
{
$fields = [];
foreach ($fieldPaths as $field) {
if (!($field instanceof FieldPath)) {
$field = FieldPath::fromString($field);
}

$fields[] = [
'fieldPath' => $this->valueMapper->escapeFieldPath($field)
'fieldPath' => $field->pathString()
];
}

Expand Down Expand Up @@ -282,7 +288,11 @@ public function where($fieldPath, $operator, $value)
));
}

$escapedFieldPath = $this->valueMapper->escapeFieldPath($fieldPath);
if (!($fieldPath instanceof FieldPath)) {
$fieldPath = FieldPath::fromString($fieldPath);
}

$escapedPathString = $fieldPath->pathString();

$operator = array_key_exists($operator, $this->shortOperators)
? $this->shortOperators[$operator]
Expand All @@ -307,7 +317,7 @@ public function where($fieldPath, $operator, $value)
$filter = [
'unaryFilter' => [
'field' => [
'fieldPath' => $escapedFieldPath
'fieldPath' => $escapedPathString
],
'op' => $unaryOperator
]
Expand All @@ -316,7 +326,7 @@ public function where($fieldPath, $operator, $value)
$filter = [
'fieldFilter' => [
'field' => [
'fieldPath' => $escapedFieldPath,
'fieldPath' => $escapedPathString,
],
'op' => $operator,
'value' => $this->valueMapper->encodeValue($value)
Expand Down Expand Up @@ -373,11 +383,15 @@ public function orderBy($fieldPath, $direction = self::DIR_ASCENDING)
);
}

if (!($fieldPath instanceof FieldPath)) {
$fieldPath = FieldPath::fromString($fieldPath);
}

return $this->newQuery([
'orderBy' => [
[
'field' => [
'fieldPath' => $this->valueMapper->escapeFieldPath($fieldPath)
'fieldPath' => $fieldPath->pathString()
],
'direction' => $direction
]
Expand Down
2 changes: 2 additions & 0 deletions Firestore/src/SnapshotTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,11 @@ private function getDocumentReference(
// manually construct a document path.
$hasSpecialChars = preg_match('/[!@#$%^&*(),.?":{}|<>]/', $name) === 1;

//@codeCoverageIgnoreStart
if (!$hasSpecialChars) {
throw $e;
}
//@codeCoverageIgnoreEnd

$base = $this->databaseName($projectId, $database);
$name = $base .'/documents/'. $name;
Expand Down
14 changes: 11 additions & 3 deletions Firestore/src/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ public function set(DocumentReference $document, array $fields, array $options =
* as a dot-delimited string (i.e. `foo.bar`), or an instance of
* {@see Google\Cloud\Firestore\FieldPath}. Nested arrays are not allowed.
*
* Please note that conflicting paths will result in an exception. Paths
* conflict when one path indicates a location nested within another path.
* For instance, path `a.b` cannot be set directly if path `a` is also
* provided.
*
* Example:
* ```
* $transaction->update($document, [
Expand Down Expand Up @@ -300,12 +305,15 @@ public function set(DocumentReference $document, array $fields, array $options =
* ]);
* ```
*
* @codingStandardsIgnoreStart
* @param DocumentReference $document The document to modify or replace.
* @param array[] $data A list of arrays of form `[FieldPath|string $path, mixed $value]`.
* @param array[] $data A list of arrays of form
* `[FieldPath|string $path, mixed $value]`.
* @param array $options Configuration options
* @return Transaction
* @codingStandardsIgnoreEnd
* @throws \InvalidArgumentException If data is given in an invalid format
* or is empty.
* @throws \InvalidArgumentException If any field paths are empty.
* @throws \InvalidArgumentException If field paths conflict.
*/
public function update(DocumentReference $document, array $data, array $options = [])
{
Expand Down
Loading