455 lines
18 KiB
PHP

<?php
declare(strict_types=1);
namespace BitBadger\PgSQL\Documents;
use PDOStatement;
/** Document manipulation functions */
class Document
{
/** JSON Mapper instance to use for creating a domain type instance from a document */
private static ?\JsonMapper $mapper = null;
/**
* Map a domain type from the JSON document retrieved
*
* @param string $columnName The name of the column from the database
* @param array $result An associative array with a single result to be mapped
* @param class-string<Type> $className The name of the class onto which the JSON will be mapped
* @return Type The domain type
*/
public static function mapDocFromJson(string $columnName, array $result, string $className): mixed
{
if (is_null(self::$mapper)) {
self::$mapper = new \JsonMapper();
}
$mapped = new $className();
self::$mapper->map(json_decode($result[$columnName]), $mapped);
return $mapped;
}
/**
* Map a domain type from the JSON document retrieved
*
* @param array $result An associative array with a single result to be mapped
* @param class-string<Type> $className The name of the class onto which the JSON will be mapped
* @return Type The domain type
*/
public static function mapFromJson(array $result, string $className): mixed
{
return self::mapDocFromJson('data', $result, $className);
}
/**
* Execute a document-focused statement that does not return results
*
* @param string $query The query to be executed
* @param string $docId The ID of the document on which action should be taken
* @param array|object $document The array or object representing the document
*/
private static function executeNonQuery(string $query, string $docId, array|object $document)
{
$nonQuery = pdo()->prepare($query);
$nonQuery->bindParam('@id', $docId);
$nonQuery->bindParam('@data', Query::jsonbDocParam($document));
$nonQuery->execute();
}
/**
* Insert a document
*
* @param string $tableName The name of the table into which a document should be inserted
* @param string $docId The ID of the document to be inserted
* @param array|object $document The array or object representing the document
*/
public static function insert(string $tableName, string $docId, array|object $document)
{
self::executeNonQuery(Query::insert($tableName), $docId, $document);
}
/**
* Save (upsert) a document
*
* @param string $tableName The name of the table into which a document should be inserted
* @param string $docId The ID of the document to be inserted
* @param array|object $document The array or object representing the document
*/
public static function save(string $tableName, string $docId, array|object $document)
{
self::executeNonQuery(Query::save($tableName), $docId, $document);
}
/**
* Count all documents in a table
*
* @param string $tableName The name of the table in which documents should be counted
* @return int The number of documents in the table
*/
public static function countAll(string $tableName): int
{
$result = pdo()->query(Query::countAll($tableName))->fetch(\PDO::FETCH_ASSOC);
return intval($result['it']);
}
/**
* Count documents in a table by JSON containment `@>`
*
* @param string $tableName The name of the table in which documents should be counted
* @param array|object $criteria The criteria for the JSON containment query
* @return int The number of documents in the table matching the JSON containment query
*/
public static function countByContains(string $tableName, array|object $criteria): int
{
$query = pdo()->prepare(Query::countByContains($tableName));
$query->bindParam('@criteria', Query::jsonbDocParam($criteria));
$query->execute();
$result = $query->fetch(\PDO::FETCH_ASSOC);
return intval($result['it']);
}
/**
* Count documents in a table by JSON Path match `@?`
*
* @param string $tableName The name of the table in which documents should be counted
* @param string $jsonPath The JSON Path to be matched
* @return int The number of documents in the table matching the JSON Path
*/
public static function countByJsonPath(string $tableName, string $jsonPath): int
{
$query = pdo()->prepare(Query::countByContains($tableName));
$query->bindParam('@path', $jsonPath);
$query->execute();
$result = $query->fetch(\PDO::FETCH_ASSOC);
return intval($result['it']);
}
/**
* Determine if a document exists for the given ID
*
* @param string $tableName The name of the table in which existence should be checked
* @param string $docId The ID of the document whose existence should be checked
* @return bool True if the document exists, false if not
*/
public static function existsById(string $tableName, string $docId): bool
{
$query = pdo()->prepare(Query::existsById($tableName));
$query->bindParam('@id', $docId);
$query->execute();
$result = $query->fetch(\PDO::FETCH_ASSOC);
return boolval($result['it']);
}
/**
* Determine if documents exist by JSON containment `@>`
*
* @param string $tableName The name of the table in which existence should be checked
* @param array|object $criteria The criteria for the JSON containment query
* @return int True if any documents in the table match the JSON containment query, false if not
*/
public static function existsByContains(string $tableName, array|object $criteria): bool
{
$query = pdo()->prepare(Query::existsByContains($tableName));
$query->bindParam('@criteria', Query::jsonbDocParam($criteria));
$query->execute();
$result = $query->fetch(\PDO::FETCH_ASSOC);
return boolval($result['it']);
}
/**
* Determine if documents exist by JSON Path match `@?`
*
* @param string $tableName The name of the table in which existence should be checked
* @param string $jsonPath The JSON Path to be matched
* @return int True if any documents in the table match the JSON Path, false if not
*/
public static function existsByJsonPath(string $tableName, string $jsonPath): bool
{
$query = pdo()->prepare(Query::existsByJsonPath($tableName));
$query->bindParam('@path', $jsonPath);
$query->execute();
$result = $query->fetch(\PDO::FETCH_ASSOC);
return boolval($result['it']);
}
/**
* Map the results of a query to domain type objects
*
* @param \PDOStatement $stmt The statement with the query to be run
* @param class-string<Type> $className The type of document to be mapped
* @return array<Type> The documents matching the query
*/
private static function mapResults(\PDOStatement $stmt, string $className): array
{
return array_map(fn ($it) => self::mapFromJson($it, $className), $stmt->fetchAll(\PDO::FETCH_ASSOC));
}
/**
* Retrieve all documents in a table
*
* @param string $tableName The table from which all documents should be retrieved
* @param class-string<Type> $className The type of document to be retrieved
* @return array<Type> An array of documents
*/
public static function findAll(string $tableName, string $className): array
{
return self::mapResults(pdo()->query(Query::selectFromTable($tableName)), $className);
}
/**
* Retrieve a document by its ID
*
* @param string $tableName The table from which a document should be retrieved
* @param string $docId The ID of the document to retrieve
* @param class-string<Type> $className The type of document to retrieve
* @return Type|null The document, or null if it is not found
*/
public static function findById(string $tableName, string $docId, string $className): mixed
{
$query = pdo()->prepare(Query::findById($tableName));
$query->bindParam(':id', $docId);
$query->execute();
$result = $query->fetch(\PDO::FETCH_ASSOC);
return $result ? self::mapFromJson($result, $className) : null;
}
/**
* Create a JSON containment query
*
* @param string $tableName The table from which documents should be retrieved
* @param array|object $criteria The criteria for the JSON containment query
* @return \PDOStatement An executed query ready to be fetched
*/
private static function queryByContains(string $tableName, array|object $criteria): \PDOStatement
{
$query = pdo()->prepare(Query::findByContains($tableName));
$query->bindParam('@criteria', Query::jsonbDocParam($criteria));
$query->execute();
return $query;
}
/**
* Retrieve documents in a table via JSON containment `@>`
*
* @param string $tableName The table from which documents should be retrieved
* @param array|object $criteria The criteria for the JSON containment query
* @param class-string<Type> $className The type of document to be retrieved
* @return array<Type> Documents matching the JSON containment query
*/
public static function findByContains(string $tableName, array|object $criteria, string $className): array
{
return self::mapResults(self::queryByContains($tableName, $criteria), $className);
}
/**
* Retrieve the first matching document via JSON containment `@>`
*
* @param string $tableName The table from which documents should be retrieved
* @param array|object $criteria The criteria for the JSON containment query
* @param class-string<Type> $className The type of document to be retrieved
* @return Type|null The document, or null if none match
*/
public static function findFirstByContains(string $tableName, array|object $criteria, string $className): mixed
{
$query = self::queryByContains($tableName, $criteria);
$result = $query->fetch(\PDO::FETCH_ASSOC);
return $result ? self::mapFromJson($result, $className) : null;
}
/**
* Retrieve documents in a table via JSON Path match `@?`
*
* @param string $tableName The table from which documents should be retrieved
* @param string $jsonPath The JSON Path to be matched
* @return \PDOStatement An executed query ready to be fetched
*/
private static function queryByJsonPath(string $tableName, string $jsonPath): \PDOStatement
{
$query = pdo()->prepare(Query::findByJsonPath($tableName));
$query->bindParam('@path', $jsonPath);
$query->execute();
return $query;
}
/**
* Retrieve documents in a table via JSON Path match `@?`
*
* @param string $tableName The table from which documents should be retrieved
* @param string $jsonPath The JSON Path to be matched
* @param class-string<Type> $className The type of document to be retrieved
* @return array<Type> Documents matching the JSON Path
*/
public static function findByJsonPath(string $tableName, string $jsonPath, string $className): array
{
return self::mapResults(self::queryByJsonPath($tableName, $jsonPath), $className);
}
/**
* Retrieve the first matching document via JSON Path match `@?`
*
* @param string $tableName The table from which documents should be retrieved
* @param string $jsonPath The JSON Path to be matched
* @param class-string<Type> $className The type of document to be retrieved
* @return Type|null The document, or null if none match
*/
public static function findFirstByJsonPath(string $tableName, string $jsonPath, string $className): mixed
{
$query = self::queryByJsonPath($tableName, $jsonPath);
$result = $query->fetch(\PDO::FETCH_ASSOC);
return $result ? self::mapFromJson($result, $className) : null;
}
/**
* Update a full document
*
* @param string $tableName The table in which the document should be updated
* @param string $docId The ID of the document to be updated
* @param array|object $document The document to be updated
*/
public static function updateFull(string $tableName, string $docId, array|object $document)
{
self::executeNonQuery(Query::updateFull($tableName), $docId, $document);
}
/**
* Update a partial document by its ID
*
* @param string $tableName The table in which the document should be updated
* @param string $docId The ID of the document to be updated
* @param array|object $document The partial document to be updated
*/
public static function updatePartialById(string $tableName, string $docId, array|object $document)
{
self::executeNonQuery(Query::updatePartialById($tableName), $docId, $document);
}
/**
* Update partial documents by JSON containment `@>`
*
* @param string $tableName The table in which documents should be updated
* @param array|object $criteria The JSON containment criteria
* @param array|object $document The document to be updated
*/
public static function updatePartialByContains(string $tableName, array|object $criteria, array|object $document)
{
$query = pdo()->prepare(Query::updatePartialByContains($tableName));
$query->bindParam('@data', Query::jsonbDocParam($document));
$query->bindParam('@criteria', Query::jsonbDocParam($criteria));
$query->execute();
}
/**
* Update partial documents by JSON Path match `@?`
*
* @param string $tableName The table in which documents should be updated
* @param string $jsonPath The JSON Path to be matched
* @param array|object $document The document to be updated
*/
public static function updatePartialByJsonPath(string $tableName, string $jsonPath, array|object $document)
{
$query = pdo()->prepare(Query::updatePartialByContains($tableName));
$query->bindParam('@data', Query::jsonbDocParam($document));
$query->bindParam('@path', $jsonPath);
$query->execute();
}
/**
* Delete a document by its ID
*
* @param string $tableName The table from which a document should be deleted
* @param string $docId The ID of the document to be deleted
*/
public static function deleteById(string $tableName, string $docId)
{
self::executeNonQuery(Query::deleteById($tableName), $docId, []);
}
/**
* Delete documents by JSON containment `@>`
*
* @param string $tableName The table from which documents should be deleted
* @param array|object $criteria The criteria for the JSON containment query
*/
public static function deleteByContains(string $tableName, array|object $criteria)
{
$query = pdo()->prepare(Query::deleteByContains($tableName));
$query->bindParam('@criteria', Query::jsonbDocParam($criteria));
$query->execute();
}
/**
* Delete documents by JSON Path match `@?`
*
* @param string $tableName The table from which documents should be deleted
* @param string $jsonPath The JSON Path expression to be matched
*/
public static function deleteByJsonPath(string $tableName, string $jsonPath)
{
$query = pdo()->prepare(Query::deleteByJsonPath($tableName));
$query->bindParam('@path', $jsonPath);
$query->execute();
}
// TODO: custom
/**
* Create and execute a custom query
*
* @param string $sql The SQL query to execute
* @param array $params An associative array of parameters for the SQL query
* @return PDOStatement The query, executed and ready to be fetched
*/
private static function createCustomQuery(string $sql, array $params): PDOStatement
{
$query = pdo()->prepare($sql);
foreach ($params as $name => $value) {
$query->bindParam($name, $value);
}
$query->execute();
return $query;
}
/**
* Retrieve documents via a custom query and mapping
*
* @param string $sql The SQL query to execute
* @param array $params An associative array of parameters for the SQL query
* @param callable $mapFunc A function that expects an associative array and returns a value of the desired type
* @param class-string<Type> $className The type of document to be mapped
* @return array<Type> The documents matching the query
*/
public static function customList(string $sql, array $params, string $className, callable $mapFunc): array
{
return array_map(
fn ($it) => $mapFunc($it, $className),
self::createCustomQuery($sql, $params)->fetchAll(\PDO::FETCH_ASSOC));
}
/**
* Retrieve a document via a custom query and mapping
*
* @param string $sql The SQL query to execute
* @param array $params An associative array of parameters for the SQL query
* @param callable $mapFunc A function that expects an associative array and returns a value of the desired type
* @param class-string<Type> $className The type of document to be mapped
* @return ?Type The document matching the query, or null if none is found
*/
public static function customSingle(string $sql, array $params, string $className, callable $mapFunc): mixed
{
$result = self::createCustomQuery($sql, $params)->fetch(\PDO::FETCH_ASSOC);
return $result ? $mapFunc($result, $className) : null;
}
/**
* Execute a custom query that does not return a result
*
* @param string $sql The SQL query to execute
* @param array $params An associative array of parameters for the SQL query
*/
public static function customNonQuery(string $sql, array $params)
{
self::createCustomQuery($sql, $params);
}
}