Skip to content

Commit

Permalink
[update] - query builder patch
Browse files Browse the repository at this point in the history
  • Loading branch information
iloElias committed Oct 5, 2024
1 parent 0e1f7cd commit b934377
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 171 deletions.
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Maestro Documentation
# Maestro Documentation

[![Maintainer](http://img.shields.io/badge/maintainer-@iloElias-blue.svg)](https://github.com/iloElias)
[![Maintainer](http://img.shields.io/badge/maintainer-@dhenriquearantes-blue.svg)](https://github.com/dhenriquearantes)
[![Package](https://img.shields.io/badge/package-iloelias/maestro-orange.svg)](https://packagist.org/packages/ilias/maestro)
Expand All @@ -7,7 +8,7 @@

Maestro is a PHP library designed to facilitate the creation and management of PostgreSQL database schemas and tables. It allows developers to define schemas and tables using PHP classes, providing a clear and structured way to manage database definitions and relationships.

### Table of Contents
## Table of Contents

- [Introduction](#introduction)
- [Installation](#installation)
Expand All @@ -30,58 +31,58 @@ Maestro is a PHP library designed to facilitate the creation and management of P
- [Defining a Schema and Tables](#defining-a-schema-and-tables)
- [Generating SQL Queries](#generating-sql-queries)

### Installation
## Installation

To install Maestro, use Composer:

```sh
composer require ilias/maestro
```

### Schema and Table Classes
## Schema and Table Classes

Maestro uses abstract classes for schemas and tables. Developers extend these classes to define their own schemas and tables.

### Defining Schemas and Tables
## Defining Schemas and Tables

#### Schema Class
### Schema Class

A schema class extends the `Schema` abstract class. It contains table attributes that are typed with the table classes.

#### Table Class
### Table Class

A table class extends the `Table` abstract class. It can define columns as class properties, specifying their types and optional default values.

#### Unique Columns
### Unique Columns

You can specify columns that should be unique by overriding the `tableUniqueColumns` method in your table class.
You can also define a column as unique by adding the `@unique` clause to the documentation of the attribute you want to make unique. This format will not work if the `tableUniqueColumns` method is overridden.

#### Default Values
### Default Values

Columns can have default values. If a default value is a PostgreSQL function, it should be defined as a `PostgresFunction` type to ensure it is not quoted in the final SQL query.

### DatabaseManager
## DatabaseManager

The `DatabaseManager` class provides methods to create schemas, tables, and manage foreign key constraints.

#### Creating Schemas and Tables
### Creating Schemas and Tables

The `createSchema` and `createTable` methods generate SQL queries to create schemas and tables. The `createTablesForSchema` method handles the creation of all tables within a schema and their foreign key constraints.

#### Foreign Key Constraints
### Foreign Key Constraints

Foreign key constraints are added using `ALTER TABLE` statements after the tables are created.

#### Executing Queries
### Executing Queries

The `executeQuery` method executes the generated SQL queries using a PDO instance.

### Query Builders
## Query Builders

Maestro provides query builder classes for common SQL operations: `Select`, `Insert`, `Update`, and `Delete`.

#### Select
### Select

The `Select` class allows you to build and execute SELECT queries.

Expand All @@ -99,7 +100,7 @@ $sql = $select->getSql();
$params = $select->getParameters();
```

#### Insert
### Insert

The `Insert` class allows you to build and execute INSERT queries.

Expand All @@ -119,7 +120,7 @@ $sql = $insert->getSql();
$params = $insert->getParameters();
```

#### Update
### Update

The `Update` class allows you to build and execute UPDATE queries.

Expand All @@ -136,7 +137,7 @@ $sql = $update->getSql();
$params = $update->getParameters();
```

#### Delete
### Delete

The `Delete` class allows you to build and execute DELETE queries.

Expand Down
4 changes: 4 additions & 0 deletions example/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ final class User extends Table

public function __construct(
string $nickname,
string $firstName,
string $lastName,
string $email,
string $password,
bool $active,
Timestamp $createdIn
) {
$this->nickname = $nickname;
$this->email = $email;
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->password = $password;
$this->active = $active;
$this->createdIn = $createdIn;
Expand Down
22 changes: 17 additions & 5 deletions maestro.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
<?php
use Ilias\Maestro\Database\Delete;

require_once("./vendor/autoload.php");

use Ilias\Maestro\Core\Maestro;
use Ilias\Maestro\Core\Manager;
use Ilias\Maestro\Database\Insert;
use Ilias\Maestro\Database\PDOConnection;
use Ilias\Maestro\Database\Select;
use Ilias\Maestro\Types\Timestamp;
use Ilias\Maestro\Utils\Utils;
use Maestro\Example\MaestroDb;
use Maestro\Example\User;

require_once("./vendor/autoload.php");

// $coreDatabase = new Manager();
// $agrofastDB = new MaestroDb();
$coreDatabase = new Manager();
$agrofastDB = new MaestroDb();
// new User("nickname", "email", "password", true, new Timestamp());

// print implode("\n", $coreDatabase->createDatabase($agrofastDB, false)) . "\n";
// print implode("\n", $coreDatabase->createDatabase($agrofastDB, true)) . "\n";

$insert = new Insert(Maestro::SQL_NO_PREDICT, PDOConnection::getInstance());
$user = new User("nickname'-- drop table", 'John', 'Doe', 'email@example.com', 'password', true, new Timestamp());
$result = $insert->into($user)->values($user)->returning(['id'])->execute();
var_dump($result);

var_dump([new stdClass() => "test"]);
$delete = new Delete(Maestro::SQL_NO_PREDICT, PDOConnection::getInstance());
$delete->from($user)->where(['id' => $result[0]['id']])->execute();

45 changes: 17 additions & 28 deletions src/Abstract/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ abstract class Query
protected array $where = [];
private ?PDOStatement $stmt = null;
private bool $isBinded = false;
protected string $query = '';

public function __construct(
protected string $behavior = Maestro::SQL_STRICT,
Expand Down Expand Up @@ -57,10 +58,16 @@ public function in(array $conditions): static

protected function storeParameter(string $name, mixed $value): void
{
if (is_int($value)) {
if (!is_bool($value) && in_array($value, Expression::DEFAULT_REPLACE_EXPRESSIONS)) {
$this->parameters[$name] = Expression::DEFAULT;
return;
}
if (is_null($value)) {
$this->parameters[$name] = Expression::NULL;
} elseif (is_int($value)) {
$this->parameters[$name] = $value;
} elseif (is_bool($value)) {
$this->parameters[$name] = $value ? 'true' : 'false';
$this->parameters[$name] = $value ? Expression::TRUE : Expression::FALSE;
} elseif (is_object($value) && is_subclass_of($value, Query::class)) {
$this->parameters[$name] = "({$value})";
} elseif (is_object($value) && $value instanceof Expression) {
Expand Down Expand Up @@ -123,37 +130,19 @@ protected function validateTableName(string $table): string

public function bindParameters(?PDO $pdo = null): Query
{
$query = $this->getSql();
foreach ($this->parameters as $key => $value) {
$query = str_replace($key, $value, $query);
}
$this->query = $query;
if (!empty($this->pdo) || !empty($pdo)) {
if (!$this->isBinded) {
$stmt = $this->pdo->prepare($this->getSql());
foreach ($this->parameters as $key => $value) {
if (is_null($value)) {
$stmt->bindValue($key, $value, PDO::PARAM_NULL);
} elseif (is_bool($value)) {
$stmt->bindValue($key, $value, PDO::PARAM_BOOL);
} elseif (is_int($value)) {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
} elseif (is_string($value) || $value instanceof \DateTime) {
$stmt->bindValue($key, (string) $value, PDO::PARAM_STR);
} else {
throw new InvalidArgumentException("Unsupported parameter type: " . gettype($value));
}
}
$stmt = $this->pdo->prepare($query);
$this->isBinded = true;
$this->stmt = $stmt;
}
return $this;
}
throw new Exception("No PDO connection provided.");
}

public function forceBindeParameters(): string
{
$query = $this->getSql();
foreach ($this->parameters as $key => $value) {
$query = str_replace($key, $value, $query);
}
return $query;
return $this;
}

public function execute(): array
Expand All @@ -170,6 +159,6 @@ public function execute(): array

public function __toString(): string
{
return $this->forceBindeParameters();
return $this->bindParameters()->query;
}
}
4 changes: 4 additions & 0 deletions src/Database/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class Expression
public const LOCALTIMESTAMP = 'LOCALTIMESTAMP';
public const RANDOM_UUID = 'gen_random_uuid()';

public const DEFAULT_REPLACE_EXPRESSIONS = [
self::RANDOM_UUID,
];

public function __construct(
private string $expression,
) {
Expand Down
13 changes: 3 additions & 10 deletions src/Database/Insert.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,14 @@ private function registerValue($column, $value)
$column = Utils::sanitizeForPostgres($column);
$this->columns[] = $column;
$paramName = ":$column";
$this->storeParameter($paramName, $value);
$this->values[] = $paramName;
$this->parameters[$paramName] = $value;
}

public function values(Table|array $data): Insert
{
if (is_object($data)) {
foreach ((array)$data as $column => $value) {
$this->registerValue($column, $value);
}
}
if (is_array($data)) {
foreach ($data as $column => $value) {
$this->registerValue($column, $value);
}
foreach ((array) $data as $column => $value) {
$this->registerValue($column, $value);
}
return $this;
}
Expand Down
91 changes: 0 additions & 91 deletions tests/integration/IntegrationTest.php
Original file line number Diff line number Diff line change
@@ -1,91 +0,0 @@
<?php

namespace Maestro\Tests\Integration;

use Ilias\Maestro\Database\Select;
use Ilias\Maestro\Core\Maestro;
use Ilias\Maestro\Core\Manager;
use Ilias\Maestro\Database\Delete;
use Ilias\Maestro\Database\Insert;
use Ilias\Maestro\Database\PDOConnection;
use Ilias\Maestro\Database\Update;
use Maestro\Example\Hr;
use Maestro\Example\User;
use PHPUnit\Framework\TestCase;
use PDO;

class IntegrationTest extends TestCase
{
private PDO $pdo;

protected function setUp(): void
{
$this->pdo = PDOConnection::getInstance();
}

public function testCreateSchema()
{
$coreDatabase = new Manager();
$coreDatabase->executeQuery($this->pdo,
$coreDatabase->createSchema(Hr::class)
);
}

public function testCreateTable()
{
$coreDatabase = new Manager();
$coreDatabase->executeQuery($this->pdo,
$coreDatabase->createTable(User::class)
);
}

public function testSelectIntegration()
{
$select = new Select(Maestro::SQL_NO_PREDICT, $this->pdo);
$select->from([User::class], ['id', 'email'])->where(['email' => 'email@example.com']);

$stmt = $this->pdo->prepare($select->getSql());
$stmt->execute($select->getParameters());
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

$this->assertNotEmpty($result);
$this->assertEquals('email@example.com', $result[0]['email']);
}

public function testInsertIntegration()
{
$insert = new Insert(Maestro::SQL_NO_PREDICT, $this->pdo);
$data = ['nickname' => 'nickname', 'email' => 'email@example.com', 'password' => 'password'];
$insert->into(User::class)->values($data);

$stmt = $this->pdo->prepare($insert->getSql());
$stmt->execute($insert->getParameters());

$this->assertEquals(1, $stmt->rowCount());
}

public function testUpdateIntegration()
{
$update = new Update(Maestro::SQL_NO_PREDICT, $this->pdo);
$data = ['nickname' => 'updated_nickname'];
$conditions = ['email' => 'email@example.com'];
$update->table(User::class)->set('nickname', 'updated_nickname')->where($conditions);

$stmt = $this->pdo->prepare($update->getSql());
$stmt->execute($update->getParameters());

$this->assertEquals(1, $stmt->rowCount());
}

public function testDeleteIntegration()
{
$delete = new Delete(Maestro::SQL_NO_PREDICT, $this->pdo);
$conditions = ['email' => 'email@example.com'];
$delete->from(User::class)->where($conditions);

$stmt = $this->pdo->prepare($delete->getSql());
$stmt->execute($delete->getParameters());

$this->assertEquals(1, $stmt->rowCount());
}
}
2 changes: 1 addition & 1 deletion tests/unit/DeleteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function testDeleteWithMultipleConditions()
$delete->from($table::tableName())->where($conditions);

$expectedSql = "DELETE FROM user WHERE email = :where_email AND active = :where_active";
$expectedParams = [':where_email' => "'email@example.com'", ':where_active' => 'false'];
$expectedParams = [':where_email' => "'email@example.com'", ':where_active' => 'FALSE'];

$this->assertEquals($expectedSql, $delete->getSql());
$this->assertEquals($expectedParams, $delete->getParameters());
Expand Down
Loading

0 comments on commit b934377

Please sign in to comment.