Initial SQLite development #1
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "bit-badger/pdo-document",
|
"name": "bit-badger/pdo-document",
|
||||||
"require": {
|
"require": {
|
||||||
"netresearch/jsonmapper": "^4"
|
"netresearch/jsonmapper": "^4",
|
||||||
|
"ext-pdo": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^11"
|
"phpunit/phpunit": "^11"
|
||||||
|
|
6
composer.lock
generated
6
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "38b71eefd32cb528da22dd5908fb8d02",
|
"content-hash": "eada4b7eb6f976e0aaf0b54b92e2ca32",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "netresearch/jsonmapper",
|
"name": "netresearch/jsonmapper",
|
||||||
|
@ -1696,7 +1696,9 @@
|
||||||
"stability-flags": [],
|
"stability-flags": [],
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {
|
||||||
|
"ext-pdo": "*"
|
||||||
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,4 +34,19 @@ class Document
|
||||||
{
|
{
|
||||||
Custom::nonQuery(Query::save($tableName), Parameters::json('@data', $document), $pdo);
|
Custom::nonQuery(Query::save($tableName), Parameters::json('@data', $document), $pdo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update (replace) an entire document by its ID
|
||||||
|
*
|
||||||
|
* @param string $tableName The table in which the document should be updated
|
||||||
|
* @param mixed $docId The ID of the document to be updated
|
||||||
|
* @param array|object $document The document to be updated
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function update(string $tableName, mixed $docId, array|object $document, ?PDO $pdo = null): void
|
||||||
|
{
|
||||||
|
Custom::nonQuery(Query::update($tableName),
|
||||||
|
array_merge(Parameters::id($docId), Parameters::json('@data', $document)), $pdo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
44
src/Exists.php
Normal file
44
src/Exists.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BitBadger\PDODocument;
|
||||||
|
|
||||||
|
use BitBadger\PDODocument\Mapper\ExistsMapper;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions to determine if documents exist
|
||||||
|
*/
|
||||||
|
class Exists
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if a document exists for the given ID
|
||||||
|
*
|
||||||
|
* @param string $tableName The name of the table in which document existence should be determined
|
||||||
|
* @param mixed $docId The ID of the document whose existence should be determined
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @return bool True if the document exists, false if not
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byId(string $tableName, mixed $docId, ?PDO $pdo = null): bool
|
||||||
|
{
|
||||||
|
return Custom::scalar(Query\Exists::byId($tableName), Parameters::id($docId), new ExistsMapper(), $pdo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a document exists using a comparison on JSON fields
|
||||||
|
*
|
||||||
|
* @param string $tableName The name of the table in which document existence should be determined
|
||||||
|
* @param Field[] $fields The field comparison to match
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`)
|
||||||
|
* @return bool True if any documents match the field comparison, false if not
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byFields(string $tableName, array $fields, ?PDO $pdo = null,
|
||||||
|
string $conjunction = 'AND'): bool
|
||||||
|
{
|
||||||
|
$namedFields = Parameters::nameFields($fields);
|
||||||
|
return Custom::scalar(Query\Exists::byFields($tableName, $namedFields, $conjunction),
|
||||||
|
Parameters::addFields($namedFields, []), new ExistsMapper(), $pdo);
|
||||||
|
}
|
||||||
|
}
|
78
src/Find.php
Normal file
78
src/Find.php
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BitBadger\PDODocument;
|
||||||
|
|
||||||
|
use BitBadger\PDODocument\Mapper\DocumentMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions to find documents
|
||||||
|
*/
|
||||||
|
class Find
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Retrieve all documents in the given table
|
||||||
|
*
|
||||||
|
* @template TDoc The type of document to be retrieved
|
||||||
|
* @param string $tableName The table from which documents should be retrieved
|
||||||
|
* @param class-string<TDoc> $className The name of the class to be retrieved
|
||||||
|
* @return DocumentList<TDoc> A list of all documents from the table
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function all(string $tableName, string $className): DocumentList
|
||||||
|
{
|
||||||
|
return Custom::list(Query::selectFromTable($tableName), [], new DocumentMapper($className));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a document by its ID (returns false if not found)
|
||||||
|
*
|
||||||
|
* @template TDoc The type of document to be retrieved
|
||||||
|
* @param string $tableName The table from which the document should be retrieved
|
||||||
|
* @param mixed $docId The ID of the document to retrieve
|
||||||
|
* @param class-string<TDoc> $className The name of the class to be retrieved
|
||||||
|
* @return false|TDoc The document if it exists, false if not
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byId(string $tableName, mixed $docId, string $className): mixed
|
||||||
|
{
|
||||||
|
return Custom::single(Query\Find::byId($tableName), Parameters::id($docId), new DocumentMapper($className));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve documents via a comparison on JSON fields
|
||||||
|
*
|
||||||
|
* @template TDoc The type of document to be retrieved
|
||||||
|
* @param string $tableName The table from which documents should be retrieved
|
||||||
|
* @param array|Field[] $fields The field comparison to match
|
||||||
|
* @param class-string<TDoc> $className The name of the class to be retrieved
|
||||||
|
* @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`)
|
||||||
|
* @return DocumentList<TDoc> A list of documents matching the given field comparison
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byFields(string $tableName, array $fields, string $className,
|
||||||
|
string $conjunction = 'AND'): DocumentList
|
||||||
|
{
|
||||||
|
$namedFields = Parameters::nameFields($fields);
|
||||||
|
return Custom::list(Query\Find::byFields($tableName, $namedFields, $conjunction),
|
||||||
|
Parameters::addFields($namedFields, []), new DocumentMapper($className));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve documents via a comparison on JSON fields, returning only the first result
|
||||||
|
*
|
||||||
|
* @template TDoc The type of document to be retrieved
|
||||||
|
* @param string $tableName The table from which the document should be retrieved
|
||||||
|
* @param array|Field[] $fields The field comparison to match
|
||||||
|
* @param class-string<TDoc> $className The name of the class to be retrieved
|
||||||
|
* @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`)
|
||||||
|
* @return false|TDoc The first document if any matches are found, false otherwise
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function firstByFields(string $tableName, array $fields, string $className,
|
||||||
|
string $conjunction = 'AND'): mixed
|
||||||
|
{
|
||||||
|
$namedFields = Parameters::nameFields($fields);
|
||||||
|
return Custom::single(Query\Find::byFields($tableName, $namedFields, $conjunction),
|
||||||
|
Parameters::addFields($namedFields, []), new DocumentMapper($className));
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BitBadger\PDODocument\Mapper;
|
namespace BitBadger\PDODocument\Mapper;
|
||||||
|
|
||||||
use BitBadger\PDODocument\DocumentException;
|
use BitBadger\PDODocument\DocumentException;
|
||||||
|
use JsonMapper;
|
||||||
use JsonMapper_Exception;
|
use JsonMapper_Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +12,7 @@ use JsonMapper_Exception;
|
||||||
* @template TDoc The type of document returned by this mapper
|
* @template TDoc The type of document returned by this mapper
|
||||||
* @implements Mapper<TDoc> Provide a mapping from JSON
|
* @implements Mapper<TDoc> Provide a mapping from JSON
|
||||||
*/
|
*/
|
||||||
class JsonMapper implements Mapper
|
class DocumentMapper implements Mapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -35,7 +36,7 @@ class JsonMapper implements Mapper
|
||||||
if (is_null($json)) {
|
if (is_null($json)) {
|
||||||
throw new DocumentException("Could not map document for $this->className: " . json_last_error_msg());
|
throw new DocumentException("Could not map document for $this->className: " . json_last_error_msg());
|
||||||
}
|
}
|
||||||
return (new \JsonMapper())->map($json, $this->className);
|
return (new JsonMapper())->map($json, $this->className);
|
||||||
} catch (JsonMapper_Exception $ex) {
|
} catch (JsonMapper_Exception $ex) {
|
||||||
throw new DocumentException("Could not map document for $this->className", previous: $ex);
|
throw new DocumentException("Could not map document for $this->className", previous: $ex);
|
||||||
}
|
}
|
44
src/Patch.php
Normal file
44
src/Patch.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BitBadger\PDODocument;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions to patch (partially update) documents
|
||||||
|
*/
|
||||||
|
class Patch
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Patch a document by its ID
|
||||||
|
*
|
||||||
|
* @param string $tableName The table in which the document should be patched
|
||||||
|
* @param mixed $docId The ID of the document to be patched
|
||||||
|
* @param array|object $patch The object with which the document should be patched (will be JSON-encoded)
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @throws DocumentException If any is encountered (database mode must be set)
|
||||||
|
*/
|
||||||
|
public static function byId(string $tableName, mixed $docId, array|object $patch, ?PDO $pdo = null): void
|
||||||
|
{
|
||||||
|
Custom::nonQuery(Query\Patch::byId($tableName),
|
||||||
|
array_merge(Parameters::id($docId), Parameters::json('@data', $patch)), $pdo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch documents using a comparison on JSON fields
|
||||||
|
*
|
||||||
|
* @param string $tableName The table in which documents should be patched
|
||||||
|
* @param array|Field[] $fields The field comparison to match
|
||||||
|
* @param array|object $patch The object with which the documents should be patched (will be JSON-encoded)
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`)
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byFields(string $tableName, array $fields, array|object $patch, ?PDO $pdo = null,
|
||||||
|
string $conjunction = 'AND'): void
|
||||||
|
{
|
||||||
|
$namedFields = Parameters::nameFields($fields);
|
||||||
|
Custom::nonQuery(Query\Patch::byFields($tableName, $namedFields, $conjunction),
|
||||||
|
Parameters::addFields($namedFields, Parameters::json('@data', $patch)), $pdo);
|
||||||
|
}
|
||||||
|
}
|
70
src/Query/RemoveFields.php
Normal file
70
src/Query/RemoveFields.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BitBadger\PDODocument\Query;
|
||||||
|
|
||||||
|
use BitBadger\PDODocument\Configuration;
|
||||||
|
use BitBadger\PDODocument\DocumentException;
|
||||||
|
use BitBadger\PDODocument\Field;
|
||||||
|
use BitBadger\PDODocument\Mode;
|
||||||
|
use BitBadger\PDODocument\Query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries to remove fields from documents
|
||||||
|
*
|
||||||
|
* _NOTE: When using these queries to build custom functions, be aware that different databases use significantly
|
||||||
|
* different syntax. The `$parameters` passed to these functions should be run through `Parameters::fieldNames`
|
||||||
|
* function to generate them appropriately for the database currently being targeted._
|
||||||
|
*/
|
||||||
|
class RemoveFields
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an UPDATE statement to remove fields from a JSON document
|
||||||
|
*
|
||||||
|
* @param string $tableName The name of the table in which documents should be manipulated
|
||||||
|
* @param array $parameters The parameter list for the query
|
||||||
|
* @param string $whereClause The body of the WHERE clause for the update
|
||||||
|
* @return string The UPDATE statement to remove fields from a JSON document
|
||||||
|
* @throws DocumentException If the database mode has not been set
|
||||||
|
*/
|
||||||
|
public static function update(string $tableName, array $parameters, string $whereClause): string
|
||||||
|
{
|
||||||
|
switch (Configuration::$mode) {
|
||||||
|
case Mode::PgSQL:
|
||||||
|
return "UPDATE $tableName SET data = data - " . array_keys($parameters)[0] . " WHERE $whereClause";
|
||||||
|
case Mode::SQLite:
|
||||||
|
$paramNames = implode(', ', array_keys($parameters));
|
||||||
|
return "UPDATE $tableName SET data = json_remove(data, $paramNames) WHERE $whereClause";
|
||||||
|
default:
|
||||||
|
throw new DocumentException('Database mode not set; cannot generate field removal query');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to remove fields from a document by the document's ID
|
||||||
|
*
|
||||||
|
* @param string $tableName The name of the table in which the document should be manipulated
|
||||||
|
* @param array $parameters The parameter list for the query
|
||||||
|
* @return string The UPDATE statement to remove fields from a document by its ID
|
||||||
|
* @throws DocumentException If the database mode has not been set
|
||||||
|
*/
|
||||||
|
public static function byId(string $tableName, array $parameters): string
|
||||||
|
{
|
||||||
|
return self::update($tableName, $parameters, Query::whereById());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to remove fields from documents via a comparison on JSON fields within the document
|
||||||
|
*
|
||||||
|
* @param string $tableName The name of the table in which documents should be manipulated
|
||||||
|
* @param array|Field[] $fields The field comparison to match
|
||||||
|
* @param array $parameters The parameter list for the query
|
||||||
|
* @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`)
|
||||||
|
* @return string The UPDATE statement to remove fields from documents via field comparison
|
||||||
|
* @throws DocumentException If the database mode has not been set
|
||||||
|
*/
|
||||||
|
public static function byFields(string $tableName, array $fields, array $parameters,
|
||||||
|
string $conjunction = 'AND'): string
|
||||||
|
{
|
||||||
|
return self::update($tableName, $parameters, Query::whereByFields($fields, $conjunction));
|
||||||
|
}
|
||||||
|
}
|
46
src/RemoveFields.php
Normal file
46
src/RemoveFields.php
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BitBadger\PDODocument;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions to remove fields from documents
|
||||||
|
*/
|
||||||
|
class RemoveFields
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Remove fields from a document by the document's ID
|
||||||
|
*
|
||||||
|
* @param string $tableName The table in which the document should have fields removed
|
||||||
|
* @param mixed $docId The ID of the document from which fields should be removed
|
||||||
|
* @param array|string[] $fieldNames The names of the fields to be removed
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byId(string $tableName, mixed $docId, array $fieldNames, ?PDO $pdo = null): void
|
||||||
|
{
|
||||||
|
$nameParams = Parameters::fieldNames('@name', $fieldNames);
|
||||||
|
Custom::nonQuery(Query\RemoveFields::byId($tableName, $nameParams),
|
||||||
|
array_merge(Parameters::id($docId), $nameParams), $pdo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove fields from documents via a comparison on a JSON field in the document
|
||||||
|
*
|
||||||
|
* @param string $tableName The table in which documents should have fields removed
|
||||||
|
* @param array|Field[] $fields The field comparison to match
|
||||||
|
* @param array|string[] $fieldNames The names of the fields to be removed
|
||||||
|
* @param PDO|null $pdo The database connection to use (optional; will obtain one if not provided)
|
||||||
|
* @param string $conjunction How to handle multiple conditions (optional; defaults to `AND`)
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function byFields(string $tableName, array $fields, array $fieldNames, ?PDO $pdo = null,
|
||||||
|
string $conjunction = 'AND'): void
|
||||||
|
{
|
||||||
|
$nameParams = Parameters::fieldNames('@name', $fieldNames);
|
||||||
|
$namedFields = Parameters::nameFields($fields);
|
||||||
|
Custom::nonQuery(Query\RemoveFields::byFields($tableName, $namedFields, $nameParams, $conjunction),
|
||||||
|
Parameters::addFields($namedFields, $nameParams), $pdo);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ namespace Test\Unit\Mapper;
|
||||||
|
|
||||||
use BitBadger\PDODocument\DocumentException;
|
use BitBadger\PDODocument\DocumentException;
|
||||||
use BitBadger\PDODocument\Field;
|
use BitBadger\PDODocument\Field;
|
||||||
use BitBadger\PDODocument\Mapper\JsonMapper;
|
use BitBadger\PDODocument\Mapper\DocumentMapper;
|
||||||
use PHPUnit\Framework\Attributes\TestDox;
|
use PHPUnit\Framework\Attributes\TestDox;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
@ -21,26 +21,26 @@ class TestDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for the JsonMapper class
|
* Unit tests for the DocumentMapper class
|
||||||
*/
|
*/
|
||||||
class JsonMapperTest extends TestCase
|
class DocumentMapperTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testConstructorSucceedsWithDefaultField(): void
|
public function testConstructorSucceedsWithDefaultField(): void
|
||||||
{
|
{
|
||||||
$mapper = new JsonMapper(Field::class);
|
$mapper = new DocumentMapper(Field::class);
|
||||||
$this->assertEquals('data', $mapper->fieldName, 'Default field name should have been "data"');
|
$this->assertEquals('data', $mapper->fieldName, 'Default field name should have been "data"');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstructorSucceedsWithSpecifiedField(): void
|
public function testConstructorSucceedsWithSpecifiedField(): void
|
||||||
{
|
{
|
||||||
$mapper = new JsonMapper(Field::class, 'json');
|
$mapper = new DocumentMapper(Field::class, 'json');
|
||||||
$this->assertEquals('json', $mapper->fieldName, 'Field name not recorded correctly');
|
$this->assertEquals('json', $mapper->fieldName, 'Field name not recorded correctly');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[TestDox('Map succeeds with valid JSON')]
|
#[TestDox('Map succeeds with valid JSON')]
|
||||||
public function testMapSucceedsWithValidJSON(): void
|
public function testMapSucceedsWithValidJSON(): void
|
||||||
{
|
{
|
||||||
$doc = (new JsonMapper(TestDocument::class))->map(['data' => '{"id":7,"subDoc":{"id":22,"name":"tester"}}']);
|
$doc = (new DocumentMapper(TestDocument::class))->map(['data' => '{"id":7,"subDoc":{"id":22,"name":"tester"}}']);
|
||||||
$this->assertNotNull($doc, 'The document should not have been null');
|
$this->assertNotNull($doc, 'The document should not have been null');
|
||||||
$this->assertEquals(7, $doc->id, 'ID not filled correctly');
|
$this->assertEquals(7, $doc->id, 'ID not filled correctly');
|
||||||
$this->assertNotNull($doc->subDoc, 'The sub-document should not have been null');
|
$this->assertNotNull($doc->subDoc, 'The sub-document should not have been null');
|
||||||
|
@ -52,6 +52,6 @@ class JsonMapperTest extends TestCase
|
||||||
public function testMapFailsWithInvalidJSON(): void
|
public function testMapFailsWithInvalidJSON(): void
|
||||||
{
|
{
|
||||||
$this->expectException(DocumentException::class);
|
$this->expectException(DocumentException::class);
|
||||||
(new JsonMapper(TestDocument::class))->map(['data' => 'this is not valid']);
|
(new DocumentMapper(TestDocument::class))->map(['data' => 'this is not valid']);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user