diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml index 882e3a6..3d32dc7 100644 --- a/.github/workflows/php81.yaml +++ b/.github/workflows/php81.yaml @@ -105,7 +105,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2 with: php-version: '8.1' coverage-file: 'php-8.1-coverage.xml' diff --git a/.github/workflows/php82.yaml b/.github/workflows/php82.yaml index 1e85684..8184a72 100644 --- a/.github/workflows/php82.yaml +++ b/.github/workflows/php82.yaml @@ -105,7 +105,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2 with: php-version: '8.2' coverage-file: 'php-8.2-coverage.xml' diff --git a/.github/workflows/php83.yaml b/.github/workflows/php83.yaml index 79d3820..759ac12 100644 --- a/.github/workflows/php83.yaml +++ b/.github/workflows/php83.yaml @@ -105,7 +105,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2 with: php-version: '8.3' coverage-file: 'php-8.3-coverage.xml' diff --git a/.github/workflows/php84.yaml b/.github/workflows/php84.yaml index 4b233b8..23e4be1 100644 --- a/.github/workflows/php84.yaml +++ b/.github/workflows/php84.yaml @@ -100,4 +100,15 @@ jobs: uses: actions/upload-artifact@v4 with: name: code-coverage - path: php-8.4-coverage.xml \ No newline at end of file + path: php-8.4-coverage.xml + + + code-coverage: + name: Coverage + needs: test + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2 + with: + php-version: '8.4' + coverage-file: 'php-8.4-coverage.xml' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/php85.yaml b/.github/workflows/php85.yaml index 31ebd38..0a7f629 100644 --- a/.github/workflows/php85.yaml +++ b/.github/workflows/php85.yaml @@ -47,7 +47,7 @@ jobs: with: php-version: 8.5 extensions: mysqli, mbstring, sqlsrv - tools: phpunit:11.5.27, composer + tools: phpunit:11.5.46, composer - name: Install ODBC Driver for SQL Server run: | @@ -106,7 +106,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2 with: php-version: '8.5' coverage-file: 'php-8.5-coverage.xml' @@ -116,13 +116,13 @@ jobs: code-quality: name: Code Quality needs: test - uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@main + uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@v1.2 secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} release-prod: name: Prepare Production Release Branch / Publish Release needs: [code-coverage, code-quality] - uses: WebFiori/workflows/.github/workflows/release-php.yaml@main + uses: WebFiori/workflows/.github/workflows/release-php.yaml@v1.2 with: branch: 'main' \ No newline at end of file diff --git a/WebFiori/Database/AbstractQuery.php b/WebFiori/Database/AbstractQuery.php index e225d7a..e1d21e2 100644 --- a/WebFiori/Database/AbstractQuery.php +++ b/WebFiori/Database/AbstractQuery.php @@ -258,12 +258,20 @@ public function delete() { * the method is called on. * */ - public function drop() { + public function drop(bool $ifExists = false) { $table = $this->getTable(); - $this->setQuery('drop table '.$table->getName().';'); + $this->setQuery($this->buildDropQuery($table->getName(), $ifExists)); return $this; } + + /** + * Builds the DROP TABLE query. Override in subclasses for DBMS-specific syntax. + */ + protected function buildDropQuery(string $tableName, bool $ifExists): string { + $ifExistsStr = $ifExists ? 'if exists ' : ''; + return 'drop table ' . $ifExistsStr . $tableName . ';'; + } /** * Constructs a query which can be used to drop a column from associated * table. diff --git a/WebFiori/Database/Attributes/ForeignKey.php b/WebFiori/Database/Attributes/ForeignKey.php index 4a83fbf..66a81c1 100644 --- a/WebFiori/Database/Attributes/ForeignKey.php +++ b/WebFiori/Database/Attributes/ForeignKey.php @@ -3,6 +3,9 @@ use Attribute; +/** + * Defines a foreign key constraint and optionally a belongsTo relationship. + */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class ForeignKey { public function __construct( @@ -11,7 +14,9 @@ public function __construct( public array $columns = [], public ?string $name = null, public string $onUpdate = 'set null', - public string $onDelete = 'set null' + public string $onDelete = 'set null', + public ?string $property = null, + public ?string $entity = null ) { if ($column !== null && !empty($columns)) { throw new InvalidAttributeException( diff --git a/WebFiori/Database/Attributes/HasMany.php b/WebFiori/Database/Attributes/HasMany.php new file mode 100644 index 0000000..4b91193 --- /dev/null +++ b/WebFiori/Database/Attributes/HasMany.php @@ -0,0 +1,18 @@ +getTables() as $tableObj) { - if ($tableObj->getColsCount() != 0) { - $generatedQuery .= $tableObj->toSQL()."\n"; + try { + foreach ($this->getTables() as $tableObj) { + if ($tableObj->getColsCount() != 0) { + $this->table($tableObj->getNormalName())->createTable()->execute(); + $created[] = $tableObj; + } + } + } catch (DatabaseException $e) { + foreach (array_reverse($created) as $tableObj) { + try { + $this->table($tableObj->getNormalName())->drop()->execute(); + } catch (DatabaseException $dropError) { + // Continue cleanup + } } + throw $e; } - $this->getQueryGenerator()->setQuery($generatedQuery, true); - - return $this->getQueryGenerator(); } /** * Constructs a query which can be used to remove a record from the diff --git a/WebFiori/Database/MsSql/MSSQLQuery.php b/WebFiori/Database/MsSql/MSSQLQuery.php index 2098c54..d1554de 100644 --- a/WebFiori/Database/MsSql/MSSQLQuery.php +++ b/WebFiori/Database/MsSql/MSSQLQuery.php @@ -27,6 +27,13 @@ public function __construct() { $this->bindings = []; } + protected function buildDropQuery(string $tableName, bool $ifExists): string { + if ($ifExists) { + return "if object_id('{$tableName}', 'U') is not null drop table {$tableName};"; + } + return "drop table {$tableName};"; + } + public function addBinding(Column $col, $value) { $this->bindings[] = $value; } diff --git a/WebFiori/Database/Repository/AbstractRepository.php b/WebFiori/Database/Repository/AbstractRepository.php index c49cd24..8035b66 100644 --- a/WebFiori/Database/Repository/AbstractRepository.php +++ b/WebFiori/Database/Repository/AbstractRepository.php @@ -1,7 +1,11 @@ db = $db; } + /** + * Specify relationships to eager load. + * + * @param string|array $relations Relationship name(s) to load. + * + * @return static Clone with eager loading configured. + */ + public function with(string|array $relations): static { + $clone = clone $this; + $clone->eagerLoad = is_array($relations) ? $relations : [$relations]; + return $clone; + } + + /** + * Specify belongsTo relationships to load via JOIN. + * + * Only works with belongsTo relationships (N:1). Throws exception + * if used with hasMany to prevent cartesian product issues. + * + * @param string|array $relations Relationship name(s) to load via JOIN. + * + * @return static Clone with JOIN loading configured. + * + * @throws RepositoryException If relation is hasMany. + */ + public function withJoin(string|array $relations): static { + $relations = is_array($relations) ? $relations : [$relations]; + $relationships = $this->discoverRelationships(); + + foreach ($relations as $relation) { + if (!isset($relationships[$relation])) { + throw new RepositoryException("Unknown relationship: {$relation}"); + } + if ($relationships[$relation]['type'] === Relation::HAS_MANY) { + throw new RepositoryException( + "Cannot use withJoin() for hasMany relation '{$relation}'. Use with() instead to avoid cartesian product." + ); + } + } + + $clone = clone $this; + $clone->joinLoad = $relations; + return $clone; + } + /** * Returns the total number of records in the table. * @@ -56,7 +109,7 @@ public function deleteAll(): void { * * @param mixed $id The ID of the record to delete, or null to use $this->id. * - * @throws \InvalidArgumentException If no ID is provided and $this has no ID. + * @throws RepositoryException If no ID is provided and $this has no ID. */ public function deleteById(mixed $id = null): void { $id = $id ?? $this->getEntityId(); @@ -75,11 +128,16 @@ public function deleteById(mixed $id = null): void { * @return T[] Array of all entities. */ public function findAll(): array { + if (!empty($this->joinLoad)) { + return $this->findAllWithJoin(); + } + $result = $this->db->table($this->getTableName()) ->select() ->execute(); - return array_map(fn($row) => $this->toEntity($row), $result->fetchAll()); + $entities = array_map(fn($row) => $this->toEntity($row), $result->fetchAll()); + return $this->loadRelations($entities); } /** @@ -89,19 +147,30 @@ public function findAll(): array { * * @return T|null The entity if found, null otherwise. * - * @throws \InvalidArgumentException If no ID is provided and $this has no ID. + * @throws RepositoryException If no ID is provided and $this has no ID. */ public function findById(mixed $id = null): ?object { $id = $id ?? $this->getEntityId(); if ($id === null) { throw new RepositoryException('Cannot find: no ID provided'); } + + if (!empty($this->joinLoad)) { + return $this->findByIdWithJoin($id); + } + $result = $this->db->table($this->getTableName()) ->select() ->where($this->getIdField(), $id) ->execute(); - return $result->getCount() > 0 ? $this->toEntity($result->fetch()) : null; + if ($result->getCount() === 0) { + return null; + } + + $entities = [$this->toEntity($result->fetch())]; + $loaded = $this->loadRelations($entities); + return $loaded[0]; } /** @@ -111,7 +180,7 @@ public function findById(mixed $id = null): ?object { * * @return T|null Fresh entity from database, or null if not found. * - * @throws \InvalidArgumentException If no entity provided and $this has no ID. + * @throws RepositoryException If no entity provided and $this has no ID. */ public function reload(?object $entity = null): ?object { if ($entity === null) { @@ -159,7 +228,8 @@ public function paginate(int $page = 1, int $perPage = 20, array $orderBy = []): } $result = $query->execute(); - $items = array_map(fn($row) => $this->toEntity($row), $result->fetchAll()); + $entities = array_map(fn($row) => $this->toEntity($row), $result->fetchAll()); + $items = $this->loadRelations($entities); return new Page($items, $page, $perPage, $total); } @@ -201,7 +271,8 @@ public function paginateByCursor( array_pop($rows); } - $items = array_map(fn($row) => $this->toEntity($row), $rows); + $entities = array_map(fn($row) => $this->toEntity($row), $rows); + $items = $this->loadRelations($entities); $nextCursor = null; @@ -221,7 +292,7 @@ public function paginateByCursor( * * @param T|null $entity The entity to save, or null to save $this. * - * @throws \InvalidArgumentException If no entity is provided and $this has no entity properties. + * @throws RepositoryException If no entity is provided and $this has no entity properties. */ public function save(?object $entity = null): void { if ($entity === null && !property_exists($this, $this->getIdField())) { @@ -306,6 +377,16 @@ protected function getDatabase(): Database { return $this->db; } + /** + * Returns the table definition class for relationship discovery. + * Override this to use a separate table class (clean architecture). + * + * @return string|null Table class name or null to use $this. + */ + protected function getTableClass(): ?string { + return null; + } + /** * Returns the name of the ID/primary key field. * @@ -337,4 +418,344 @@ abstract protected function toArray(object $entity): array; * @return T The mapped entity. */ abstract protected function toEntity(array $row): object; + + /** + * Creates a related entity instance from row data using reflection. + */ + private function createRelatedEntity(array $config, array $row): object { + $entityClass = $config['entity'] ?? null; + + if ($entityClass === null || !class_exists($entityClass)) { + return (object) $row; + } + + $entity = new $entityClass(); + $ref = new ReflectionClass($entity); + + foreach ($row as $key => $value) { + $propName = $this->toCamelCase($key); + if ($ref->hasProperty($propName)) { + $prop = $ref->getProperty($propName); + $prop->setAccessible(true); + $prop->setValue($entity, $this->castValue($prop, $value)); + } elseif ($ref->hasProperty($key)) { + $prop = $ref->getProperty($key); + $prop->setAccessible(true); + $prop->setValue($entity, $this->castValue($prop, $value)); + } + } + + return $entity; + } + + /** + * Cast value to property type. + */ + private function castValue(\ReflectionProperty $prop, mixed $value): mixed { + if ($value === null) { + return null; + } + + $type = $prop->getType(); + if ($type instanceof \ReflectionNamedType) { + return match ($type->getName()) { + 'int' => (int) $value, + 'float' => (float) $value, + 'bool' => (bool) $value, + 'string' => (string) $value, + default => $value + }; + } + + return $value; + } + + /** + * Load eager-loaded relationships onto entities. + */ + private function loadRelations(array $entities): array { + if (empty($this->eagerLoad) || empty($entities)) { + return $entities; + } + + $relationships = $this->discoverRelationships(); + + foreach ($this->eagerLoad as $relation) { + if (!isset($relationships[$relation])) { + throw new RepositoryException("Unknown relationship: {$relation}"); + } + + $config = $relationships[$relation]; + + if ($config['type'] === Relation::HAS_MANY) { + $this->loadHasMany($entities, $config); + } elseif ($config['type'] === Relation::BELONGS_TO) { + $this->loadBelongsTo($entities, $config); + } + } + + return $entities; + } + + /** + * Execute findAll with JOIN for belongsTo relations (single query). + */ + private function findAllWithJoin(): array { + $relationships = $this->discoverRelationships(); + $query = $this->buildJoinQuery($relationships); + $rows = $query->execute()->fetchAll(); + + return $this->hydrateJoinedRows($rows, $this->joinLoad, $relationships); + } + + /** + * Execute findById with JOIN for belongsTo relations (single query). + */ + private function findByIdWithJoin(mixed $id): ?object { + $relationships = $this->discoverRelationships(); + $query = $this->buildJoinQuery($relationships); + $query->where($this->getIdField(), $id); + $rows = $query->execute()->fetchAll(); + + if (empty($rows)) { + return null; + } + + $entities = $this->hydrateJoinedRows($rows, $this->joinLoad, $relationships); + return $entities[0] ?? null; + } + + /** + * Build query with LEFT JOINs for belongsTo relations. + */ + private function buildJoinQuery(array $relationships): AbstractQuery { + $this->db->clear(); + $query = $this->db->table($this->getTableName()); + + foreach ($this->joinLoad as $relation) { + $config = $relationships[$relation]; + $relatedTable = $this->db->getTable($config['table']); + + // Select related columns with aliases to avoid duplicates + $cols = []; + if ($relatedTable !== null) { + foreach ($relatedTable->getColsKeys() as $colKey) { + $cols[$colKey] = ['as' => $relation . '_' . $colKey]; + } + } + + $query = $query->leftJoin( + $this->db->table($config['table'])->select($cols) + )->on($config['foreignKey'], $config['ownerKey'] ?? 'id'); + } + + return $query->select(); + } + + /** + * Hydrate joined rows into entities with related objects. + */ + private function hydrateJoinedRows(array $rows, array $joinRelations, array $relationships): array { + $entities = []; + + foreach ($rows as $row) { + $entity = $this->toEntity($row); + + foreach ($joinRelations as $relation) { + $config = $relationships[$relation]; + $prefix = $relation . '_'; + + // Extract prefixed columns for this relation + $relatedData = []; + foreach ($row as $key => $value) { + if (str_starts_with($key, $prefix)) { + $relatedData[substr($key, strlen($prefix))] = $value; + } + } + + // Query builder renames conflicting 'id' to 'right_id' + if (isset($row['right_id']) && !isset($relatedData['id'])) { + $relatedData['id'] = $row['right_id']; + } + + $ownerKey = $config['ownerKey'] ?? 'id'; + $hasData = isset($relatedData[$ownerKey]) && $relatedData[$ownerKey] !== null; + + $related = $hasData ? $this->createRelatedEntity($config, $relatedData) : null; + $this->setPropertyValue($entity, $config['property'], $related); + } + + $entities[] = $entity; + } + + return $entities; + } + + /** + * Load hasMany relationship (1+1 queries - preload strategy). + */ + private function loadHasMany(array &$entities, array $config): void { + $localKey = $config['localKey'] ?? $this->getIdField(); + $ids = []; + + foreach ($entities as $entity) { + $id = $this->getPropertyValue($entity, $localKey); + if ($id !== null) { + $ids[] = $id; + } + } + + if (empty($ids)) { + return; + } + + $fkColumn = $config['foreignKey']; + $related = $this->db->table($config['table']) + ->select() + ->whereIn($fkColumn, array_unique($ids)) + ->execute() + ->fetchAll(); + + $grouped = []; + foreach ($related as $row) { + $fkValue = $row[$fkColumn] ?? $row[str_replace('-', '_', $fkColumn)] ?? null; + if ($fkValue !== null) { + $grouped[$fkValue][] = $this->createRelatedEntity($config, $row); + } + } + + foreach ($entities as $entity) { + $id = $this->getPropertyValue($entity, $localKey); + $this->setPropertyValue($entity, $config['property'], $grouped[$id] ?? []); + } + } + + /** + * Load belongsTo relationship (1+1 queries - smart strategy). + */ + private function loadBelongsTo(array &$entities, array $config): void { + $foreignKey = $config['foreignKey']; + $ownerKey = $config['ownerKey'] ?? 'id'; + $ids = []; + + foreach ($entities as $entity) { + $id = $this->getPropertyValue($entity, $foreignKey); + if ($id !== null) { + $ids[] = $id; + } + } + + if (empty($ids)) { + return; + } + + $ownerColumn = $ownerKey; + $related = $this->db->table($config['table']) + ->select() + ->whereIn($ownerColumn, array_unique($ids)) + ->execute() + ->fetchAll(); + + $indexed = []; + foreach ($related as $row) { + $keyValue = $row[$ownerColumn] ?? $row[str_replace('-', '_', $ownerColumn)] ?? null; + if ($keyValue !== null) { + $indexed[$keyValue] = $this->createRelatedEntity($config, $row); + } + } + + foreach ($entities as $entity) { + $fkValue = $this->getPropertyValue($entity, $foreignKey); + $this->setPropertyValue($entity, $config['property'], $indexed[$fkValue] ?? null); + } + } + + /** + * Discover relationships from table class attributes. + */ + private function discoverRelationships(): array { + if ($this->relationships !== null) { + return $this->relationships; + } + + $this->relationships = []; + $tableClass = $this->getTableClass() ?? static::class; + + if (!class_exists($tableClass)) { + return $this->relationships; + } + + $reflection = new ReflectionClass($tableClass); + + // Discover HasMany (class-level) + foreach ($reflection->getAttributes(HasMany::class) as $attr) { + $hasMany = $attr->newInstance(); + $table = $hasMany->table ?? $this->resolveTableName($hasMany->entity); + $this->relationships[$hasMany->property] = [ + 'type' => Relation::HAS_MANY, + 'table' => $table, + 'foreignKey' => $hasMany->foreignKey, + 'localKey' => $hasMany->localKey, + 'property' => $hasMany->property, + 'entity' => $hasMany->entity + ]; + } + + // Discover BelongsTo (ForeignKey with property) + foreach ($reflection->getProperties() as $prop) { + foreach ($prop->getAttributes(ForeignKey::class) as $attr) { + $fk = $attr->newInstance(); + if ($fk->property !== null) { + $table = $this->resolveTableName($fk->table); + $this->relationships[$fk->property] = [ + 'type' => Relation::BELONGS_TO, + 'table' => $table, + 'foreignKey' => $this->propertyToKey($prop->getName()), + 'ownerKey' => $fk->column, + 'property' => $fk->property, + 'entity' => $fk->entity + ]; + } + } + } + + return $this->relationships; + } + + /** + * Resolve table name from class (if it has #[Table] attribute). + */ + private function resolveTableName(string $classOrTable): string { + if (class_exists($classOrTable)) { + $ref = new ReflectionClass($classOrTable); + $attrs = $ref->getAttributes(Table::class); + if (!empty($attrs)) { + return $attrs[0]->newInstance()->name; + } + } + return $classOrTable; + } + + private function propertyToKey(string $name): string { + return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $name)); + } + + private function getPropertyValue(object $entity, string $property): mixed { + $camel = $this->toCamelCase($property); + if (property_exists($entity, $camel)) { + return $entity->$camel; + } + if (property_exists($entity, $property)) { + return $entity->$property; + } + return null; + } + + private function setPropertyValue(object $entity, string $property, mixed $value): void { + $entity->$property = $value; + } + + private function toCamelCase(string $key): string { + return lcfirst(str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)))); + } } diff --git a/WebFiori/Database/Repository/Relation.php b/WebFiori/Database/Repository/Relation.php new file mode 100644 index 0000000..cecef8a --- /dev/null +++ b/WebFiori/Database/Repository/Relation.php @@ -0,0 +1,10 @@ +createTables(); - $this->execute(); } /** diff --git a/examples/01-basic-connection/example.php b/examples/01-basic-connection/example.php index 4f52d82..45f0836 100644 --- a/examples/01-basic-connection/example.php +++ b/examples/01-basic-connection/example.php @@ -5,65 +5,70 @@ use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Connection Example ===\n\n"; try { // Create connection info + echo SEP; + echo "1. Creating Connection Info:\n"; $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); - echo "✓ Connection info created\n"; - echo " Database Type: ".$connection->getDatabaseType()."\n"; - echo " Host: ".$connection->getHost()."\n"; - echo " Database: ".$connection->getDBName()."\n\n"; + echo " ✓ Connection info created\n"; + echo " Database Type: ".$connection->getDatabaseType()."\n"; + echo " Host: ".$connection->getHost()."\n"; + echo " Database: ".$connection->getDBName()."\n\n"; // Establish database connection + echo SEP; + echo "2. Establishing Connection:\n"; $database = new Database($connection); - echo "✓ Database connection established\n"; + echo " ✓ Database connection established\n\n"; - // Test connection with a simple query using raw() + // Test connection with a simple query + echo SEP; + echo "3. Testing Connection:\n"; $result = $database->raw("SELECT VERSION() as version")->execute(); if ($result) { - echo "✓ Connection test successful\n"; + echo " ✓ Connection test successful\n"; $rows = $result->getRows(); if (!empty($rows)) { - echo " MySQL Version: ".$rows[0]['version']."\n"; + echo " MySQL Version: ".$rows[0]['version']."\n"; } } + echo "\n"; - // Additional connection tests using raw() with parameters - echo "\n--- Additional Connection Tests ---\n"; + // Additional connection tests + echo SEP; + echo "4. Additional Connection Info:\n"; - // Test current database $result = $database->raw("SELECT DATABASE() as current_db")->execute(); - if ($result && $result->getRowsCount() > 0) { - echo "✓ Current database: ".$result->getRows()[0]['current_db']."\n"; + echo " ✓ Current database: ".$result->getRows()[0]['current_db']."\n"; } - // Test server status $result = $database->raw("SHOW STATUS LIKE 'Uptime'")->execute(); - if ($result && $result->getRowsCount() > 0) { $uptime = $result->getRows()[0]['Value']; - echo "✓ Server uptime: ".$uptime." seconds\n"; + echo " ✓ Server uptime: ".$uptime." seconds\n"; } - // Test connection info $result = $database->raw("SELECT CONNECTION_ID() as connection_id")->execute(); - if ($result && $result->getRowsCount() > 0) { - echo "✓ Connection ID: ".$result->getRows()[0]['connection_id']."\n"; + echo " ✓ Connection ID: ".$result->getRows()[0]['connection_id']."\n"; } $result = $database->raw("SELECT USER() as user_name")->execute(); - if ($result && $result->getRowsCount() > 0) { - echo "✓ Current User: ".$result->getRows()[0]['user_name']."\n"; + echo " ✓ Current User: ".$result->getRows()[0]['user_name']."\n"; } + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; echo "Note: Make sure MySQL is running and accessible with the provided credentials.\n"; } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/02-basic-queries/example.php b/examples/02-basic-queries/example.php index baf8a26..6487328 100644 --- a/examples/02-basic-queries/example.php +++ b/examples/02-basic-queries/example.php @@ -2,223 +2,103 @@ require_once '../../vendor/autoload.php'; +use WebFiori\Database\ColOption; use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +use WebFiori\Database\DataType; use WebFiori\Database\MultiResultSet; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database CRUD Operations Example ===\n\n"; try { - // Create connection - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); - // Create a test table using raw() - echo "1. Creating test table...\n"; - $database->raw(" - CREATE TABLE IF NOT EXISTS test_users ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(150) NOT NULL, - age INT - ) - ")->execute(); - echo "✓ Test table created\n\n"; - - // Clear any existing data using raw() - $database->raw("DELETE FROM test_users")->execute(); - - // INSERT operations using raw() with parameters - echo "2. INSERT Operations with Parameters:\n"; - - $database->raw("INSERT INTO test_users (name, email, age) VALUES (?, ?, ?)", [ - 'Ahmed Hassan', 'ahmed@example.com', 30 - ])->execute(); - echo "✓ Inserted Ahmed Hassan\n"; - - $database->raw("INSERT INTO test_users (name, email, age) VALUES (?, ?, ?)", [ - 'Fatima Al-Zahra', 'fatima@example.com', 25 - ])->execute(); - echo "✓ Inserted Fatima Al-Zahra\n"; - - $database->raw("INSERT INTO test_users (name, email, age) VALUES (?, ?, ?)", [ - 'Omar Khalil', 'omar@example.com', 35 + // Create table + echo SEP; + echo "1. Creating Table:\n"; + + $database->createBlueprint('test_users')->addColumns([ + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], + 'email' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 150], + 'age' => [ColOption::TYPE => DataType::INT] + ]); + + $database->table('test_users')->drop(true)->execute(); + $database->table('test_users')->createTable()->execute(); + echo " ✓ Table created\n\n"; + + // INSERT operations + echo SEP; + echo "2. INSERT Operations:\n"; + + $database->table('test_users')->insert([ + 'cols' => ['name', 'email', 'age'], + 'values' => [ + ['Ahmed Hassan', 'ahmed@example.com', 30], + ['Fatima Al-Zahra', 'fatima@example.com', 25], + ['Omar Khalil', 'omar@example.com', 35] + ] ])->execute(); - echo "✓ Inserted Omar Khalil\n\n"; - - // SELECT operations using raw() with parameters - echo "3. SELECT Operations with Parameters:\n"; - - // Select all records - $result = $database->raw("SELECT * FROM test_users")->execute(); - echo "All users:\n"; - - foreach ($result as $user) { - echo " - {$user['name']} ({$user['email']}) - Age: {$user['age']}\n"; - } - echo "\n"; + echo " ✓ Inserted 3 users\n\n"; - // Select with condition using parameters - $result = $database->raw("SELECT * FROM test_users WHERE age > ?", [30])->execute(); - echo "Users older than 30:\n"; + // SELECT operations + echo SEP; + echo "3. SELECT Operations:\n"; + $result = $database->table('test_users')->select()->execute(); + echo " All users:\n"; foreach ($result as $user) { - echo " - {$user['name']} - Age: {$user['age']}\n"; + echo " - {$user['name']} ({$user['email']}) - Age: {$user['age']}\n"; } echo "\n"; - // Multi-parameter query - $result = $database->raw("SELECT * FROM test_users WHERE age BETWEEN ? AND ?", [25, 35])->execute(); - echo "Users between 25 and 35:\n"; - + $result = $database->table('test_users')->select()->where('age', 30, '>')->execute(); + echo " Users older than 30:\n"; foreach ($result as $user) { - echo " - {$user['name']} - Age: {$user['age']}\n"; + echo " - {$user['name']} - Age: {$user['age']}\n"; } echo "\n"; - // UPDATE operations using raw() with parameters - echo "4. UPDATE Operations with Parameters:\n"; - $database->raw("UPDATE test_users SET age = ? WHERE name = ?", [26, 'Fatima Al-Zahra'])->execute(); - echo "✓ Updated Fatima Al-Zahra's age to 26\n"; + // UPDATE operations + echo SEP; + echo "4. UPDATE Operations:\n"; - // Verify update - $result = $database->raw("SELECT * FROM test_users WHERE name = ?", ['Fatima Al-Zahra'])->execute(); + $database->table('test_users')->update(['age' => 26])->where('name', 'Fatima Al-Zahra')->execute(); + echo " ✓ Updated Fatima Al-Zahra's age to 26\n"; + $result = $database->table('test_users')->select()->where('name', 'Fatima Al-Zahra')->execute(); foreach ($result as $user) { - echo " Fatima's new age: {$user['age']}\n"; + echo " Fatima's new age: {$user['age']}\n"; } echo "\n"; - // DELETE operations using raw() with parameters - echo "5. DELETE Operations with Parameters:\n"; - $database->raw("DELETE FROM test_users WHERE name = ?", ['Omar Khalil'])->execute(); - echo "✓ Deleted Omar Khalil\n"; + // DELETE operations + echo SEP; + echo "5. DELETE Operations:\n"; - // Show remaining users - $result = $database->raw("SELECT * FROM test_users")->execute(); - echo "Remaining users:\n"; + $database->table('test_users')->delete()->where('name', 'Omar Khalil')->execute(); + echo " ✓ Deleted Omar Khalil\n"; + $result = $database->table('test_users')->select()->execute(); + echo " Remaining users:\n"; foreach ($result as $user) { - echo " - {$user['name']} ({$user['email']}) - Age: {$user['age']}\n"; - } - - // Multi-Result Query Example - echo "\n6. Multi-Result Query Example:\n"; - - // Create a stored procedure that returns multiple result sets - $database->raw("DROP PROCEDURE IF EXISTS GetUserStats")->execute(); - $database->raw(" - CREATE PROCEDURE GetUserStats() - BEGIN - SELECT 'User List' as report_type; - SELECT name, age FROM test_users ORDER BY age; - SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM test_users; - END - ")->execute(); - - // Execute the stored procedure - $result = $database->raw("CALL GetUserStats()")->execute(); - - if ($result instanceof MultiResultSet) { - echo "✓ Multi-result query executed successfully!\n"; - echo "Number of result sets: ".$result->count()."\n"; - - for ($i = 0; $i < $result->count(); $i++) { - $resultSet = $result->getResultSet($i); - echo "\nResult Set ".($i + 1).":\n"; - - foreach ($resultSet as $row) { - echo " "; - - foreach ($row as $key => $value) { - echo "$key: $value "; - } - echo "\n"; - } - } - } else { - echo "Single result set returned:\n"; - - foreach ($result as $row) { - echo " "; - - foreach ($row as $key => $value) { - echo "$key: $value "; - } - echo "\n"; - } + echo " - {$user['name']} ({$user['email']}) - Age: {$user['age']}\n"; } + echo "\n"; - // Another multi-result example with conditional logic - echo "\n7. Complex Multi-Result Example:\n"; - - $database->raw("DROP PROCEDURE IF EXISTS ComplexStats")->execute(); - $database->raw(" - CREATE PROCEDURE ComplexStats() - BEGIN - -- First result: All users - SELECT 'All Users' as section, name, email, age FROM test_users; - - -- Second result: Statistics - SELECT - 'Statistics' as section, - COUNT(*) as total_count, - MIN(age) as min_age, - MAX(age) as max_age, - AVG(age) as avg_age - FROM test_users; - - -- Third result: Age groups - SELECT - 'Age Groups' as section, - CASE - WHEN age < 30 THEN 'Young' - WHEN age >= 30 THEN 'Mature' - END as age_group, - COUNT(*) as count - FROM test_users - GROUP BY age_group; - END - ")->execute(); - - $complexResult = $database->raw("CALL ComplexStats()")->execute(); - - if ($complexResult instanceof MultiResultSet) { - echo "✓ Complex multi-result query executed!\n"; - - // Process each result set with specific handling - for ($i = 0; $i < $complexResult->count(); $i++) { - $rs = $complexResult->getResultSet($i); - - if ($rs->getRowsCount() > 0) { - $firstRow = $rs->getRows()[0]; - - if (isset($firstRow['section'])) { - echo "\n--- {$firstRow['section']} ---\n"; - - foreach ($rs as $row) { - if ($row['section'] === 'All Users') { - echo " User: {$row['name']} ({$row['email']}) - Age: {$row['age']}\n"; - } elseif ($row['section'] === 'Statistics') { - echo " Total: {$row['total_count']}, Min Age: {$row['min_age']}, Max Age: {$row['max_age']}, Avg Age: ".round($row['avg_age'], 1)."\n"; - } elseif ($row['section'] === 'Age Groups') { - echo " {$row['age_group']}: {$row['count']} users\n"; - } - } - } - } - } - } + // Cleanup + echo SEP; + echo "6. Cleanup:\n"; + $database->table('test_users')->drop()->execute(); + echo " ✓ Table dropped\n"; - // Clean up - echo "\n8. Cleanup:\n"; - $database->raw("DROP PROCEDURE IF EXISTS GetUserStats")->execute(); - $database->raw("DROP PROCEDURE IF EXISTS ComplexStats")->execute(); - $database->raw("DROP TABLE test_users")->execute(); - echo "✓ Test table and procedures dropped\n"; } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - echo "Stack trace:\n".$e->getTraceAsString()."\n"; } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/03-table-blueprints/example.php b/examples/03-table-blueprints/example.php index 1bfa82d..86e1e33 100644 --- a/examples/03-table-blueprints/example.php +++ b/examples/03-table-blueprints/example.php @@ -7,173 +7,87 @@ use WebFiori\Database\Database; use WebFiori\Database\DataType; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Table Blueprints Example ===\n\n"; try { - // Create connection - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Creating Users Table Blueprint:\n"; - // Create users table blueprint $usersTable = $database->createBlueprint('users')->addColumns([ - 'id' => [ - ColOption::TYPE => DataType::INT, - ColOption::SIZE => 11, - ColOption::PRIMARY => true, - ColOption::AUTO_INCREMENT => true - ], - 'username' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 50, - ColOption::NULL => false - ], - 'email' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 150, - ColOption::NULL => false - ], - 'created_at' => [ - ColOption::TYPE => DataType::TIMESTAMP, - ColOption::DEFAULT => 'current_timestamp' - ] + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'username' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 50], + 'email' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 150], + 'created-at' => [ColOption::TYPE => DataType::TIMESTAMP, ColOption::DEFAULT => 'current_timestamp'] ]); - echo "✓ Users table blueprint created\n"; - echo " Columns: ".implode(', ', array_keys($usersTable->getCols()))."\n\n"; + echo " ✓ Users table blueprint created\n"; + echo " Columns: ".implode(', ', $usersTable->getColsKeys())."\n\n"; + echo SEP; echo "2. Creating Posts Table Blueprint:\n"; - // Create posts table blueprint $postsTable = $database->createBlueprint('posts')->addColumns([ - 'id' => [ - ColOption::TYPE => DataType::INT, - ColOption::SIZE => 11, - ColOption::PRIMARY => true, - ColOption::AUTO_INCREMENT => true - ], - 'user-id' => [ - ColOption::TYPE => DataType::INT, - ColOption::SIZE => 11, - ColOption::NULL => false - ], - 'title' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 200, - ColOption::NULL => false - ], - 'content' => [ - ColOption::TYPE => DataType::TEXT - ], - 'created-at' => [ - ColOption::TYPE => DataType::TIMESTAMP, - ColOption::DEFAULT => 'current_timestamp' - ] + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'user-id' => [ColOption::TYPE => DataType::INT], + 'title' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 200], + 'content' => [ColOption::TYPE => DataType::TEXT], + 'created-at' => [ColOption::TYPE => DataType::TIMESTAMP, ColOption::DEFAULT => 'current_timestamp'] ]); - echo "✓ Posts table blueprint created\n"; - echo " Columns: ".implode(', ', array_keys($postsTable->getCols()))."\n\n"; + echo " ✓ Posts table blueprint created\n"; + echo " Columns: ".implode(', ', $postsTable->getColsKeys())."\n\n"; + echo SEP; echo "3. Adding Foreign Key Relationship:\n"; - // Add foreign key relationship with CASCADE actions $postsTable->addReference($usersTable, ['user-id' => 'id'], 'user_fk', 'cascade', 'cascade'); - echo "✓ Foreign key relationship added (posts.user_id -> users.id)\n\n"; - - echo "4. Creating Tables One by One:\n"; + echo " ✓ Foreign key added (posts.user-id -> users.id)\n\n"; - // Create users table first (no dependencies) - $database->table('users')->createTable(); - echo "SQL for users table:\n".$database->getLastQuery()."\n\n"; - $database->execute(); - echo "✓ Users table created\n"; + echo SEP; + echo "4. Creating Tables:\n"; - // Create posts table (depends on users) - $database->table('posts')->createTable(); - echo "SQL for posts table:\n".$database->getLastQuery()."\n\n"; - $database->execute(); - echo "✓ Posts table created\n\n"; + $database->table('users')->drop(true)->execute(); + $database->table('posts')->drop(true)->execute(); + $database->createTables(); + echo " ✓ Tables created\n\n"; - echo "5. Testing the Created Tables:\n"; + echo SEP; + echo "5. Testing Tables:\n"; - // Insert test data $database->table('users')->insert([ 'username' => 'ahmad_salem', 'email' => 'ahmad@example.com' ])->execute(); - echo "✓ Inserted test user\n"; - - // Get the user ID - $userResult = $database->table('users') - ->select(['id']) - ->where('username', 'ahmad_salem') - ->execute(); - $userId = $userResult->getRows()[0]['id']; + echo " ✓ Inserted test user\n"; $database->table('posts')->insert([ - 'user-id' => $userId, + 'user-id' => 1, 'title' => 'My First Post', 'content' => 'This is the content of my first post.' ])->execute(); - echo "✓ Inserted test post\n"; - - // Query with join to show relationship - $result = $database->raw(" - SELECT u.username, p.title, p.created_at - FROM users u - JOIN posts p ON u.id = p.user_id - ")->execute(); + echo " ✓ Inserted test post\n"; - echo "\nJoined data:\n"; + $result = $database->table('users')->select()->execute(); + echo " Users:\n"; foreach ($result as $row) { - echo " User: {$row['username']}, Post: {$row['title']}, Created: {$row['created_at']}\n"; + echo " - {$row['username']} ({$row['email']})\n"; } + echo "\n"; - echo "\n6. Using Custom Table Class:\n"; - - // Include the custom table class - require_once __DIR__.'/UserTable.php'; - - // Create an instance of the custom table - $customTable = new UserTable(); - - echo "✓ Custom UserTable class created\n"; - echo " Table name: ".$customTable->getName()."\n"; - echo " Engine: ".$customTable->getEngine()."\n"; - echo " Charset: ".$customTable->getCharSet()."\n"; - - // Generate and execute CREATE TABLE for custom table - $createQuery = $customTable->toSQL(); - echo "\nGenerated SQL for custom table:\n$createQuery\n\n"; - - // Execute the custom table creation - $database->raw($createQuery)->execute(); - echo "✓ Custom table created successfully\n"; - - // Test the custom table - $database->addTable($customTable); - $database->table('users_extended')->insert([ - 'username' => 'sara_ahmad', - 'email' => 'sara@example.com', - 'full-name' => 'Sara Ahmad Al-Mansouri' - ])->execute(); - echo "✓ Inserted test data into custom table\n"; - - // Query the custom table - $result = $database->table('users_extended')->select()->execute(); - echo "Custom table data:\n"; - foreach ($result as $row) { - echo " User: {$row['full_name']} ({$row['username']}) - Active: ".($row['is_active'] ? 'Yes' : 'No')."\n"; - } + echo SEP; + echo "6. Cleanup:\n"; + $database->table('posts')->drop()->execute(); + $database->table('users')->drop()->execute(); + echo " ✓ Tables dropped\n"; - echo "\n7. Cleanup:\n"; - $database->raw("DROP TABLE IF EXISTS users_extended")->execute(); - $database->raw("DROP TABLE IF EXISTS posts")->execute(); - $database->raw("DROP TABLE IF EXISTS users")->execute(); - echo "✓ Tables dropped\n"; } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/04-entity-mapping/example.php b/examples/04-entity-mapping/example.php index ca9f299..c82e749 100644 --- a/examples/04-entity-mapping/example.php +++ b/examples/04-entity-mapping/example.php @@ -7,14 +7,16 @@ use WebFiori\Database\Database; use WebFiori\Database\DataType; -echo "=== WebFiori Database Entity Mapping Example ===\n\n"; +const SEP = "────────────────────────────────────────────────────────────────────\n"; +echo "=== WebFiori Database Entity Mapping Example ===\n\n"; echo "This example shows entity generation and manual mapping approaches.\n\n"; try { - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Creating User Table:\n"; $userTable = $database->createBlueprint('users')->addColumns([ @@ -22,28 +24,32 @@ 'first-name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 50], 'last-name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 50], 'email' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 150], - 'age' => [ColOption::TYPE => DataType::INT, ColOption::SIZE => 3] + 'age' => [ColOption::TYPE => DataType::INT] ]); - $database->table('users')->createTable(); - $database->execute(); - echo "✓ User table created\n\n"; + $database->table('users')->drop(true)->execute(); + $database->table('users')->createTable()->execute(); + echo " ✓ User table created\n\n"; + echo SEP; echo "2. Inserting Test Data:\n"; - $database->table('users')->insert(['first-name' => 'Khalid', 'last-name' => 'Al-Rashid', 'email' => 'khalid@example.com', 'age' => 30])->execute(); - $database->table('users')->insert(['first-name' => 'Aisha', 'last-name' => 'Mahmoud', 'email' => 'aisha@example.com', 'age' => 25])->execute(); - $database->table('users')->insert(['first-name' => 'Hassan', 'last-name' => 'Al-Najjar', 'email' => 'hassan@example.com', 'age' => 35])->execute(); - echo "✓ Test users inserted\n\n"; - - // ============================================ - // APPROACH 1: Using EntityMapper (Deprecated) - // ============================================ + $database->table('users')->insert([ + 'cols' => ['first-name', 'last-name', 'email', 'age'], + 'values' => [ + ['Khalid', 'Al-Rashid', 'khalid@example.com', 30], + ['Aisha', 'Mahmoud', 'aisha@example.com', 25], + ['Hassan', 'Al-Najjar', 'hassan@example.com', 35] + ] + ])->execute(); + echo " ✓ 3 test users inserted\n\n"; + + echo SEP; echo "3. Using EntityGenerator:\n"; $entityGenerator = $userTable->getEntityGenerator('User', __DIR__, ''); $entityGenerator->generate(); - echo "✓ User entity class generated at: ".__DIR__."/User.php\n"; + echo " ✓ User entity class generated at: ".__DIR__."/User.php\n"; require_once __DIR__.'/User.php'; @@ -56,50 +62,45 @@ age: (int) $record['age'] )); - echo "Mapped users (EntityGenerator):\n"; + echo " Mapped users:\n"; foreach ($mappedUsers as $user) { - echo " - {$user->getFirstName()} {$user->getLastName()} ({$user->getEmail()})\n"; + echo " - {$user->getFirstName()} {$user->getLastName()} ({$user->getEmail()})\n"; } echo "\n"; - // ============================================ - // APPROACH 2: Manual Entity (Recommended) - // ============================================ - echo "4. Alternative: Manual Entity Mapping:\n"; + echo SEP; + echo "4. Manual Entity Mapping:\n"; - // Define entity manually (in real code, this would be in a separate file) - // See example 11-repository-pattern for full implementation - - echo "Manual entity mapping example:\n"; $result = $database->table('users')->select()->execute(); foreach ($result as $row) { - // Manual mapping - more control, no code generation $user = (object) [ 'id' => (int) $row['id'], 'firstName' => $row['first_name'], 'lastName' => $row['last_name'], 'email' => $row['email'], - 'age' => (int) $row['age'], 'fullName' => $row['first_name'].' '.$row['last_name'] ]; - echo " - {$user->fullName} ({$user->email}) - Age: {$user->age}\n"; + echo " - {$user->fullName} ({$user->email})\n"; } echo "\n"; + echo SEP; echo "5. Cleanup:\n"; - $database->raw("DROP TABLE users")->execute(); - echo "✓ User table dropped\n"; + $database->table('users')->drop()->execute(); + echo " ✓ User table dropped\n"; if (file_exists(__DIR__.'/User.php')) { unlink(__DIR__.'/User.php'); - echo "✓ Generated User.php file removed\n"; + echo " ✓ Generated User.php file removed\n"; } + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; try { - $database->raw("DROP TABLE IF EXISTS users")->execute(); + $database->table('users')->drop(true)->execute(); if (file_exists(__DIR__.'/User.php')) unlink(__DIR__.'/User.php'); } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/05-transactions/example.php b/examples/05-transactions/example.php index 35a4db7..ae34777 100644 --- a/examples/05-transactions/example.php +++ b/examples/05-transactions/example.php @@ -2,242 +2,129 @@ require_once '../../vendor/autoload.php'; +use WebFiori\Database\ColOption; use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; use WebFiori\Database\DatabaseException; +use WebFiori\Database\DataType; + +const SEP = "────────────────────────────────────────────────────────────────────\n"; echo "=== WebFiori Database Transactions Example ===\n\n"; try { - // Create connection - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); - echo "1. Setting up Test Tables:\n"; - - // Create test tables using raw() - $database->raw(" - CREATE TABLE IF NOT EXISTS accounts ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 - ) - ")->execute(); - - $database->raw(" - CREATE TABLE IF NOT EXISTS transactions ( - id INT AUTO_INCREMENT PRIMARY KEY, - from_account INT, - to_account INT, - amount DECIMAL(10,2) NOT NULL, - description VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ")->execute(); - - echo "✓ Test tables created\n\n"; - - // Clear existing data using raw() - $database->raw("DELETE FROM transactions")->execute(); - $database->raw("DELETE FROM accounts")->execute(); - - // Insert initial account data using raw() with parameters - $database->raw("INSERT INTO accounts (name, balance) VALUES (?, ?)", [ - 'Amira', 1000.00 - ])->execute(); + echo SEP; + echo "1. Setting up Tables:\n"; + + $database->createBlueprint('accounts')->addColumns([ + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], + 'balance' => [ColOption::TYPE => DataType::DECIMAL, ColOption::SIZE => 10] + ]); + + $database->createBlueprint('transactions')->addColumns([ + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'from-account' => [ColOption::TYPE => DataType::INT], + 'to-account' => [ColOption::TYPE => DataType::INT], + 'amount' => [ColOption::TYPE => DataType::DECIMAL, ColOption::SIZE => 10], + 'description' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 255] + ]); + + $database->table('transactions')->drop(true)->execute(); + $database->table('accounts')->drop(true)->execute(); + $database->createTables(); + echo " ✓ Tables created\n\n"; + + echo SEP; + echo "2. Initial Account Balances:\n"; - $database->raw("INSERT INTO accounts (name, balance) VALUES (?, ?)", [ - 'Yusuf', 500.00 + $database->table('accounts')->insert([ + 'cols' => ['name', 'balance'], + 'values' => [ + ['Amira', 1000.00], + ['Yusuf', 500.00] + ] ])->execute(); - echo "2. Initial Account Balances:\n"; - $result = $database->raw("SELECT * FROM accounts")->execute(); - + $result = $database->table('accounts')->select()->execute(); foreach ($result as $account) { - echo " {$account['name']}: $".number_format($account['balance'], 2)."\n"; + echo " {$account['name']}: $".number_format($account['balance'], 2)."\n"; } echo "\n"; - echo "3. Successful Transaction Example:\n"; + echo SEP; + echo "3. Successful Transaction:\n"; - // Successful money transfer transaction $transferAmount = 200.00; - $fromAccountId = 1; // Amira - $toAccountId = 2; // Yusuf - - $database->transaction(function (Database $db) use ($transferAmount, $fromAccountId, $toAccountId) - { - // Check if sender has sufficient balance using raw() with parameters - $senderResult = $db->raw("SELECT balance FROM accounts WHERE id = ?", [$fromAccountId])->execute(); - $senderBalance = $senderResult->getRows()[0]['balance']; - if ($senderBalance < $transferAmount) { + $database->transaction(function (Database $db) use ($transferAmount) { + $sender = $db->table('accounts')->select()->where('id', 1)->execute()->fetch(); + + if ($sender['balance'] < $transferAmount) { throw new DatabaseException("Insufficient funds"); } - // Deduct from sender using raw() with parameters - $db->raw("UPDATE accounts SET balance = ? WHERE id = ?", [ - $senderBalance - $transferAmount, $fromAccountId - ])->execute(); + $db->table('accounts')->update(['balance' => $sender['balance'] - $transferAmount])->where('id', 1)->execute(); + + $receiver = $db->table('accounts')->select()->where('id', 2)->execute()->fetch(); + $db->table('accounts')->update(['balance' => $receiver['balance'] + $transferAmount])->where('id', 2)->execute(); - // Get receiver balance using raw() with parameters - $receiverResult = $db->raw("SELECT balance FROM accounts WHERE id = ?", [$toAccountId])->execute(); - $receiverBalance = $receiverResult->getRows()[0]['balance']; - - // Add to receiver using raw() with parameters - $db->raw("UPDATE accounts SET balance = ? WHERE id = ?", [ - $receiverBalance + $transferAmount, $toAccountId - ])->execute(); - - // Record the transaction using raw() with parameters - $db->raw("INSERT INTO transactions (from_account, to_account, amount, description) VALUES (?, ?, ?, ?)", [ - $fromAccountId, $toAccountId, $transferAmount, 'Money transfer' + $db->table('transactions')->insert([ + 'from-account' => 1, + 'to-account' => 2, + 'amount' => $transferAmount, + 'description' => 'Money transfer' ])->execute(); - echo "✓ Transaction completed successfully\n"; + echo " ✓ Transaction completed\n"; }); - echo "Account balances after successful transfer:\n"; - $result = $database->raw("SELECT * FROM accounts")->execute(); - + $result = $database->table('accounts')->select()->execute(); + echo " Balances after transfer:\n"; foreach ($result as $account) { - echo " {$account['name']}: $".number_format($account['balance'], 2)."\n"; + echo " - {$account['name']}: $".number_format($account['balance'], 2)."\n"; } echo "\n"; - echo "4. Failed Transaction Example (Insufficient Funds):\n"; - - // Attempt to transfer more money than available - $largeTransferAmount = 2000.00; + echo SEP; + echo "4. Failed Transaction (Insufficient Funds):\n"; try { - $database->transaction(function (Database $db) use ($largeTransferAmount, $fromAccountId, $toAccountId) - { - // Check if sender has sufficient balance using raw() with parameters - $senderResult = $db->raw("SELECT balance FROM accounts WHERE id = ?", [$fromAccountId])->execute(); - $senderBalance = $senderResult->getRows()[0]['balance']; - - if ($senderBalance < $largeTransferAmount) { - throw new DatabaseException("Insufficient funds for transfer of $".number_format($largeTransferAmount, 2)); + $database->transaction(function (Database $db) { + $sender = $db->table('accounts')->select()->where('id', 1)->execute()->fetch(); + + if ($sender['balance'] < 2000.00) { + throw new DatabaseException("Insufficient funds for $2000.00 transfer"); } - - // This code won't be reached due to insufficient funds - $db->raw("UPDATE accounts SET balance = ? WHERE id = ?", [ - $senderBalance - $largeTransferAmount, $fromAccountId - ])->execute(); }); } catch (DatabaseException $e) { - echo "✗ Transaction failed: ".$e->getMessage()."\n"; - echo "✓ Transaction was rolled back automatically\n"; + echo " ✗ Transaction failed: ".$e->getMessage()."\n"; + echo " ✓ Rolled back automatically\n"; } - echo "Account balances after failed transaction (should be unchanged):\n"; - $result = $database->raw("SELECT * FROM accounts")->execute(); - + $result = $database->table('accounts')->select()->execute(); + echo " Balances unchanged:\n"; foreach ($result as $account) { - echo " {$account['name']}: $".number_format($account['balance'], 2)."\n"; + echo " - {$account['name']}: $".number_format($account['balance'], 2)."\n"; } echo "\n"; - echo "5. Transaction History:\n"; - $result = $database->raw(" - SELECT t.*, - a1.name as from_name, - a2.name as to_name - FROM transactions t - LEFT JOIN accounts a1 ON t.from_account = a1.id - LEFT JOIN accounts a2 ON t.to_account = a2.id - ORDER BY t.created_at - ")->execute(); - - if ($result->getRowsCount() > 0) { - foreach ($result as $transaction) { - echo " Transfer: {$transaction['from_name']} → {$transaction['to_name']}\n"; - echo " Amount: $".number_format($transaction['amount'], 2)."\n"; - echo " Date: {$transaction['created_at']}\n"; - echo " Description: {$transaction['description']}\n\n"; - } - } else { - echo " No transactions recorded\n\n"; - } + echo SEP; + echo "5. Cleanup:\n"; + $database->table('transactions')->drop()->execute(); + $database->table('accounts')->drop()->execute(); + echo " ✓ Tables dropped\n"; - echo "6. Multi-Result Transaction Analysis:\n"; - - // Create a stored procedure for transaction analysis - $database->raw("DROP PROCEDURE IF EXISTS TransactionAnalysis")->execute(); - $database->raw(" - CREATE PROCEDURE TransactionAnalysis() - BEGIN - -- Account summary - SELECT 'Account Summary' as report_type, name, balance FROM accounts ORDER BY balance DESC; - - -- Transaction summary - SELECT 'Transaction Summary' as report_type, - COUNT(*) as total_transactions, - SUM(amount) as total_amount, - AVG(amount) as avg_amount - FROM transactions; - - -- Recent transactions - SELECT 'Recent Transactions' as report_type, - t.amount, - a1.name as from_name, - a2.name as to_name, - t.created_at - FROM transactions t - LEFT JOIN accounts a1 ON t.from_account = a1.id - LEFT JOIN accounts a2 ON t.to_account = a2.id - ORDER BY t.created_at DESC - LIMIT 5; - END - ")->execute(); - - $analysisResult = $database->raw("CALL TransactionAnalysis()")->execute(); - - if (method_exists($analysisResult, 'count') && $analysisResult->count() > 1) { - echo "✓ Multi-result transaction analysis completed!\n"; - - for ($i = 0; $i < $analysisResult->count(); $i++) { - $rs = $analysisResult->getResultSet($i); - - if ($rs->getRowsCount() > 0) { - $firstRow = $rs->getRows()[0]; - - if (isset($firstRow['report_type'])) { - echo "\n--- {$firstRow['report_type']} ---\n"; - - foreach ($rs as $row) { - if ($row['report_type'] === 'Account Summary') { - echo " {$row['name']}: $".number_format($row['balance'], 2)."\n"; - } elseif ($row['report_type'] === 'Transaction Summary') { - echo " Total Transactions: {$row['total_transactions']}\n"; - echo " Total Amount: $".number_format($row['total_amount'], 2)."\n"; - echo " Average Amount: $".number_format($row['avg_amount'], 2)."\n"; - } elseif ($row['report_type'] === 'Recent Transactions') { - echo " {$row['from_name']} → {$row['to_name']}: $".number_format($row['amount'], 2)." ({$row['created_at']})\n"; - } - } - } - } - } - } - - echo "\n7. Cleanup:\n"; - $database->raw("DROP PROCEDURE IF EXISTS TransactionAnalysis")->execute(); - $database->raw("DROP TABLE transactions")->execute(); - $database->raw("DROP TABLE accounts")->execute(); - echo "✓ Test tables and procedures dropped\n"; } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - - // Clean up on error try { - $database->raw("DROP PROCEDURE IF EXISTS TransactionAnalysis")->execute(); - $database->raw("DROP TABLE IF EXISTS transactions")->execute(); - $database->raw("DROP TABLE IF EXISTS accounts")->execute(); - } catch (Exception $cleanupError) { - // Ignore cleanup errors - } + $database->table('transactions')->drop(true)->execute(); + $database->table('accounts')->drop(true)->execute(); + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/06-migrations/example.php b/examples/06-migrations/example.php index 6492b3b..e057511 100644 --- a/examples/06-migrations/example.php +++ b/examples/06-migrations/example.php @@ -6,147 +6,98 @@ use WebFiori\Database\Database; use WebFiori\Database\Schema\SchemaRunner; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Migrations Example ===\n\n"; try { - // Create connection - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Setting up Schema Runner:\n"; - // Create schema runner $runner = new SchemaRunner($connection); - - // Discover and register migration classes from directory $runner->discoverFromPath(__DIR__, ''); - - echo "✓ Schema runner created\n"; - echo "✓ Migration classes discovered from path\n"; - - // Create schema tracking table $runner->createSchemaTable(); - echo "✓ Schema tracking table created\n\n"; + echo " ✓ Schema runner created\n"; + echo " ✓ Migration classes discovered\n\n"; - echo "2. Checking Available Migrations:\n"; + echo SEP; + echo "2. Available Migrations:\n"; $changes = $runner->getChanges(); - echo "Discovered migrations:\n"; foreach ($changes as $change) { - echo " - ".$change->getName()."\n"; + echo " - ".$change->getName()."\n"; } echo "\n"; - echo "3. Running Migrations (using apply()):\n"; + echo SEP; + echo "3. Running Migrations:\n"; - // Apply all pending migrations $result = $runner->apply(); if ($result->count() > 0) { - echo "Applied migrations:\n"; foreach ($result->getApplied() as $change) { - echo " ✓ ".$change->getName()."\n"; + echo " ✓ ".$change->getName()."\n"; } } else { - echo "No migrations to apply (all up to date)\n"; - } - - if (!empty($result->getFailed())) { - echo "Failed migrations:\n"; - foreach ($result->getFailed() as $failure) { - echo " ✗ ".$failure['change']->getName().": ".$failure['error']->getMessage()."\n"; - } + echo " No migrations to apply\n"; } echo "\n"; - echo "4. Verifying Database Structure:\n"; + echo SEP; + echo "4. Verifying Structure:\n"; - // Check if table exists - $tableResult = $database->raw("SHOW TABLES LIKE 'users'")->execute(); - if ($tableResult->getRowsCount() > 0) { - echo "✓ Users table created\n"; - } - - // Check table structure $descResult = $database->raw("DESCRIBE users")->execute(); - echo "Users table columns:\n"; + echo " Users table columns:\n"; foreach ($descResult as $column) { - echo " - {$column['Field']} ({$column['Type']})\n"; - } - - // Check indexes - $indexResult = $database->raw("SHOW INDEX FROM users WHERE Key_name = 'idx_users_email'")->execute(); - if ($indexResult->getRowsCount() > 0) { - echo "✓ Email index created\n"; + echo " - {$column['Field']} ({$column['Type']})\n"; } echo "\n"; + echo SEP; echo "5. Testing Data Operations:\n"; - // Insert test data - $database->table('users')->insert([ - 'username' => 'ahmad_hassan', - 'email' => 'ahmad@example.com', - 'password-hash' => password_hash('password123', PASSWORD_DEFAULT) - ])->execute(); - $database->table('users')->insert([ - 'username' => 'fatima_ali', - 'email' => 'fatima@example.com', - 'password-hash' => password_hash('password456', PASSWORD_DEFAULT) + 'cols' => ['username', 'email', 'password-hash'], + 'values' => [ + ['ahmad_hassan', 'ahmad@example.com', password_hash('password123', PASSWORD_DEFAULT)], + ['fatima_ali', 'fatima@example.com', password_hash('password456', PASSWORD_DEFAULT)] + ] ])->execute(); + echo " ✓ Test users inserted\n"; - echo "✓ Test users inserted\n"; - - // Query data - $selectResult = $database->table('users')->select(['username', 'email', 'created-at'])->execute(); - echo "Inserted users:\n"; + $selectResult = $database->table('users')->select(['username', 'email'])->execute(); + echo " Users:\n"; foreach ($selectResult as $user) { - echo " - {$user['username']} ({$user['email']}) - {$user['created_at']}\n"; + echo " - {$user['username']} ({$user['email']})\n"; } echo "\n"; - echo "6. Checking Migration Status:\n"; - echo "Migration status:\n"; - foreach ($changes as $change) { - $status = $runner->isApplied($change->getName()) ? "✓ Applied" : "✗ Pending"; - echo " {$change->getName()}: $status\n"; - } - echo "\n"; + echo SEP; + echo "6. Rolling Back Migrations:\n"; - echo "7. Rolling Back Migrations:\n"; - - // Rollback all migrations $rolledBack = $runner->rollbackUpTo(null); - if (!empty($rolledBack)) { - echo "Rolled back migrations:\n"; foreach ($rolledBack as $change) { - echo " ✓ ".$change->getName()."\n"; + echo " ✓ Rolled back: ".$change->getName()."\n"; } - } else { - echo "No migrations to rollback\n"; - } - - // Verify rollback - $verifyResult = $database->raw("SHOW TABLES LIKE 'users'")->execute(); - if ($verifyResult->getRowsCount() == 0) { - echo "✓ Users table removed\n"; } + echo "\n"; - echo "\n8. Cleanup:\n"; + echo SEP; + echo "7. Cleanup:\n"; $runner->dropSchemaTable(); - echo "✓ Schema tracking table dropped\n"; + echo " ✓ Schema tracking table dropped\n"; + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - - // Clean up on error try { - $database->raw("DROP TABLE IF EXISTS users")->execute(); + $database->table('users')->drop(true)->execute(); $database->raw("DROP TABLE IF EXISTS schema_changes")->execute(); - } catch (Exception $cleanupError) { - // Ignore cleanup errors - } + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/07-seeders/example.php b/examples/07-seeders/example.php index 5b462f9..436c6d5 100644 --- a/examples/07-seeders/example.php +++ b/examples/07-seeders/example.php @@ -8,188 +8,99 @@ use WebFiori\Database\DataType; use WebFiori\Database\Schema\SchemaRunner; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Seeders Example ===\n\n"; try { - // Create connection - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); - echo "1. Creating Test Tables:\n"; - - // Clean up any existing tables first - $database->raw("DROP TABLE IF EXISTS categories")->execute(); - $database->raw("DROP TABLE IF EXISTS users")->execute(); + echo SEP; + echo "1. Creating Tables:\n"; - // Create users table $database->createBlueprint('users')->addColumns([ - 'id' => [ - ColOption::TYPE => DataType::INT, - ColOption::SIZE => 11, - ColOption::PRIMARY => true, - ColOption::AUTO_INCREMENT => true - ], - 'username' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 50, - ColOption::NULL => false - ], - 'email' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 150, - ColOption::NULL => false - ], - 'full-name' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 100 - ], - 'role' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 20, - ColOption::DEFAULT => 'user' - ], - 'is-active' => [ - ColOption::TYPE => DataType::BOOL, - ColOption::DEFAULT => true - ] + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'username' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 50], + 'email' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 150], + 'full-name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], + 'role' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 20, ColOption::DEFAULT => 'user'], + 'is-active' => [ColOption::TYPE => DataType::BOOL, ColOption::DEFAULT => true] ]); - // Create categories table $database->createBlueprint('categories')->addColumns([ - 'id' => [ - ColOption::TYPE => DataType::INT, - ColOption::SIZE => 11, - ColOption::PRIMARY => true, - ColOption::AUTO_INCREMENT => true - ], - 'name' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 100, - ColOption::NULL => false - ], - 'description' => [ - ColOption::TYPE => DataType::TEXT - ], - 'slug' => [ - ColOption::TYPE => DataType::VARCHAR, - ColOption::SIZE => 100, - ColOption::NULL => false - ] + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], + 'description' => [ColOption::TYPE => DataType::TEXT], + 'slug' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100] ]); - // Create tables one by one - $database->table('users')->createTable(); - $database->execute(); - echo "✓ Users table created\n"; - - $database->table('categories')->createTable(); - $database->execute(); - echo "✓ Categories table created\n\n"; + $database->table('categories')->drop(true)->execute(); + $database->table('users')->drop(true)->execute(); + $database->createTables(); + echo " ✓ Tables created\n\n"; + echo SEP; echo "2. Setting up Schema Runner:\n"; - // Create schema runner $runner = new SchemaRunner($connection); - - // Discover and register seeder classes from directory $runner->discoverFromPath(__DIR__, ''); - - echo "✓ Schema runner created\n"; - echo "✓ Seeder classes discovered from path\n"; - - // Create schema tracking table $runner->createSchemaTable(); - echo "✓ Schema tracking table created\n\n"; + echo " ✓ Schema runner created\n"; + echo " ✓ Seeder classes discovered\n\n"; - echo "3. Checking Available Seeders:\n"; + echo SEP; + echo "3. Available Seeders:\n"; $changes = $runner->getChanges(); - echo "Discovered seeders:\n"; foreach ($changes as $change) { - echo " - ".$change->getName()."\n"; + echo " - ".$change->getName()."\n"; } echo "\n"; - echo "4. Running Seeders (using apply()):\n"; + echo SEP; + echo "4. Running Seeders:\n"; - // Apply all pending seeders $result = $runner->apply(); - if ($result->count() > 0) { - echo "Applied seeders:\n"; foreach ($result->getApplied() as $change) { - echo " ✓ ".$change->getName()."\n"; + echo " ✓ ".$change->getName()."\n"; } - } else { - echo "No seeders to apply (all up to date)\n"; } echo "\n"; + echo SEP; echo "5. Verifying Seeded Data:\n"; - // Check users data $usersResult = $database->table('users')->select()->execute(); - echo "Seeded users ({$usersResult->getRowsCount()} records):\n"; + echo " Users ({$usersResult->getRowsCount()} records):\n"; foreach ($usersResult as $user) { - $status = $user['is_active'] ? 'Active' : 'Inactive'; - echo " - {$user['full_name']} (@{$user['username']}) - {$user['role']} - $status\n"; + echo " - {$user['full_name']} (@{$user['username']}) - {$user['role']}\n"; } echo "\n"; - // Check categories data $categoriesResult = $database->table('categories')->select()->execute(); - echo "Seeded categories ({$categoriesResult->getRowsCount()} records):\n"; + echo " Categories ({$categoriesResult->getRowsCount()} records):\n"; foreach ($categoriesResult as $category) { - echo " - {$category['name']} ({$category['slug']})\n"; - } - echo "\n"; - - echo "6. Checking Seeder Status:\n"; - echo "Seeder status:\n"; - foreach ($changes as $change) { - $status = $runner->isApplied($change->getName()) ? "✓ Applied" : "✗ Pending"; - echo " {$change->getName()}: $status\n"; + echo " - {$category['name']} ({$category['slug']})\n"; } echo "\n"; - echo "7. Rolling Back Seeders:\n"; - - // Rollback all seeders (note: seeders don't clear data by default) - $rolledBack = $runner->rollbackUpTo(null); - - if (!empty($rolledBack)) { - echo "Rolled back seeders (tracking removed):\n"; - foreach ($rolledBack as $change) { - echo " ✓ ".$change->getName()."\n"; - } - } else { - echo "No seeders to rollback\n"; - } - - // Note: Data remains because seeders don't implement rollback by default - $userCount = $database->table('users')->select()->execute()->getRowsCount(); - $categoryCount = $database->table('categories')->select()->execute()->getRowsCount(); - - echo "Note: Data remains after rollback (seeders don't clear data by default):\n"; - echo " Users: $userCount records\n"; - echo " Categories: $categoryCount records\n\n"; - - echo "8. Cleanup:\n"; + echo SEP; + echo "6. Cleanup:\n"; $runner->dropSchemaTable(); - $database->raw("DROP TABLE categories")->execute(); - $database->raw("DROP TABLE users")->execute(); - echo "✓ Test tables and schema tracking table dropped\n"; + $database->table('categories')->drop()->execute(); + $database->table('users')->drop()->execute(); + echo " ✓ Tables dropped\n"; + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - - // Clean up on error try { - $database->raw("DROP TABLE IF EXISTS categories")->execute(); - $database->raw("DROP TABLE IF EXISTS users")->execute(); + $database->table('categories')->drop(true)->execute(); + $database->table('users')->drop(true)->execute(); $database->raw("DROP TABLE IF EXISTS schema_changes")->execute(); - } catch (Exception $cleanupError) { - // Ignore cleanup errors - } + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/08-performance-monitoring/example.php b/examples/08-performance-monitoring/example.php index c6275a5..135485b 100644 --- a/examples/08-performance-monitoring/example.php +++ b/examples/08-performance-monitoring/example.php @@ -2,59 +2,57 @@ require_once '../../vendor/autoload.php'; +use WebFiori\Database\ColOption; use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +use WebFiori\Database\DataType; use WebFiori\Database\Performance\PerformanceAnalyzer; use WebFiori\Database\Performance\PerformanceOption; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Performance Monitoring Example ===\n\n"; try { - // 1. Setup database connection - echo "1. Setting up database connection:\n"; $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db', 'localhost'); $database = new Database($connection); - echo "✓ Database connection established\n\n"; - // 2. Configure and enable performance monitoring - echo "2. Configuring performance monitoring:\n"; + echo SEP; + echo "1. Configuring Performance Monitoring:\n"; $database->setPerformanceConfig([ PerformanceOption::ENABLED => true, - PerformanceOption::SLOW_QUERY_THRESHOLD => 50, // 50ms threshold - PerformanceOption::WARNING_THRESHOLD => 25, // 25ms warning - PerformanceOption::SAMPLING_RATE => 1.0, // Monitor all queries - PerformanceOption::MAX_SAMPLES => 1000 // Keep up to 1000 samples + PerformanceOption::SLOW_QUERY_THRESHOLD => 50, + PerformanceOption::WARNING_THRESHOLD => 25, + PerformanceOption::SAMPLING_RATE => 1.0, + PerformanceOption::MAX_SAMPLES => 1000 + ]); + echo " ✓ Performance monitoring configured\n"; + echo " - Slow query threshold: 50ms\n"; + echo " - Warning threshold: 25ms\n"; + echo " - Sampling rate: 100%\n\n"; + + echo SEP; + echo "2. Creating Test Table:\n"; + $database->createBlueprint('performance_test')->addColumns([ + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], + 'email' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 150] ]); - echo "✓ Performance monitoring configured\n"; - echo " - Slow query threshold: 50ms\n"; - echo " - Warning threshold: 25ms\n"; - echo " - Sampling rate: 100%\n\n"; - - // 3. Create test table for demonstration - echo "3. Creating test table:\n"; - $database->setQuery("DROP TABLE IF EXISTS performance_test")->execute(); - $database->setQuery(" - CREATE TABLE performance_test ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100), - email VARCHAR(150), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_email (email) - ) - ")->execute(); - echo "✓ Test table created\n\n"; - - // 4. Execute various queries with different performance characteristics - echo "4. Executing test queries:\n"; - - // Fast queries + $database->table('performance_test')->drop(true)->execute(); + $database->table('performance_test')->createTable()->execute(); + echo " ✓ Test table created\n\n"; + + echo SEP; + echo "3. Executing Test Queries:\n"; + + // Fast queries - individual inserts for performance monitoring for ($i = 1; $i <= 5; $i++) { $database->table('performance_test')->insert([ 'name' => "User $i", 'email' => "user$i@example.com" ])->execute(); } - echo "✓ Executed 5 fast INSERT queries\n"; + echo " ✓ Executed 5 INSERT queries\n"; // Medium speed queries for ($i = 1; $i <= 3; $i++) { @@ -63,86 +61,85 @@ ->where('email', "user$i@example.com") ->execute(); } - echo "✓ Executed 3 medium SELECT queries\n"; + echo " ✓ Executed 3 SELECT queries\n"; // Simulate slow queries with SLEEP - $database->setQuery("SELECT SLEEP(0.03)")->execute(); // 30ms - $database->setQuery("SELECT SLEEP(0.08)")->execute(); // 80ms - $database->setQuery("SELECT SLEEP(0.12)")->execute(); // 120ms - echo "✓ Executed 3 slow queries with artificial delays\n\n"; + $database->raw("SELECT SLEEP(0.03)")->execute(); + $database->raw("SELECT SLEEP(0.08)")->execute(); + $database->raw("SELECT SLEEP(0.12)")->execute(); + echo " ✓ Executed 3 slow queries with artificial delays\n\n"; - // 5. Analyze performance using the new PerformanceAnalyzer - echo "5. Performance Analysis:\n"; + echo SEP; + echo "4. Performance Analysis:\n"; $analyzer = $database->getPerformanceMonitor()->getAnalyzer(); - echo "Query Statistics:\n"; - echo " - Total queries: ".$analyzer->getQueryCount()."\n"; - echo " - Total execution time: ".number_format($analyzer->getTotalTime(), 2)." ms\n"; - echo " - Average execution time: ".number_format($analyzer->getAverageTime(), 2)." ms\n"; - echo " - Performance score: ".$analyzer->getScore()."\n"; - echo " - Query efficiency: ".number_format($analyzer->getEfficiency(), 1)."%\n\n"; + echo " Query Statistics:\n"; + echo " - Total queries: ".$analyzer->getQueryCount()."\n"; + echo " - Total execution time: ".number_format($analyzer->getTotalTime(), 2)." ms\n"; + echo " - Average execution time: ".number_format($analyzer->getAverageTime(), 2)." ms\n"; + echo " - Performance score: ".$analyzer->getScore()."\n"; + echo " - Query efficiency: ".number_format($analyzer->getEfficiency(), 1)."%\n\n"; - // 6. Analyze slow queries - echo "6. Slow Query Analysis:\n"; + echo SEP; + echo "5. Slow Query Analysis:\n"; $slowQueries = $analyzer->getSlowQueries(); - echo " - Slow queries found: ".$analyzer->getSlowQueryCount()."\n"; + echo " - Slow queries found: ".$analyzer->getSlowQueryCount()."\n"; if (!empty($slowQueries)) { - echo " - Slow query details:\n"; - + echo " - Slow query details:\n"; foreach ($slowQueries as $index => $metric) { $query = $metric->getQuery(); $time = $metric->getExecutionTimeMs(); $rows = $metric->getRowsAffected(); - - // Truncate long queries for display $displayQuery = strlen($query) > 60 ? substr($query, 0, 57).'...' : $query; - echo " ".($index + 1).". ".number_format($time, 2)."ms - $displayQuery ($rows rows)\n"; + echo " ".($index + 1).". ".number_format($time, 2)."ms - $displayQuery ($rows rows)\n"; } - } else { - echo " - No slow queries detected\n"; } echo "\n"; - // 7. Performance recommendations - echo "7. Performance Recommendations:\n"; + echo SEP; + echo "6. Performance Recommendations:\n"; $score = $analyzer->getScore(); $efficiency = $analyzer->getEfficiency(); switch ($score) { case PerformanceAnalyzer::SCORE_EXCELLENT: - echo " ✓ Excellent performance! Your queries are running very efficiently.\n"; + echo " ✓ Excellent performance! Your queries are running very efficiently.\n"; break; case PerformanceAnalyzer::SCORE_GOOD: - echo " ✓ Good performance overall. Consider optimizing slow queries if any.\n"; + echo " ✓ Good performance overall. Consider optimizing slow queries if any.\n"; break; case PerformanceAnalyzer::SCORE_NEEDS_IMPROVEMENT: - echo " ⚠ Performance needs improvement. Focus on optimizing slow queries.\n"; + echo " ⚠ Performance needs improvement. Focus on optimizing slow queries.\n"; break; } if ($efficiency < 80) { - echo " ⚠ Query efficiency is below 80%. Consider:\n"; - echo " - Adding database indexes\n"; - echo " - Optimizing query structure\n"; - echo " - Reviewing WHERE clauses\n"; + echo " ⚠ Query efficiency is below 80%. Consider:\n"; + echo " - Adding database indexes\n"; + echo " - Optimizing query structure\n"; + echo " - Reviewing WHERE clauses\n"; } if ($analyzer->getSlowQueryCount() > 0) { - echo " ⚠ Slow queries detected. Consider:\n"; - echo " - Adding appropriate indexes\n"; - echo " - Limiting result sets with LIMIT\n"; - echo " - Breaking complex queries into smaller ones\n"; + echo " ⚠ Slow queries detected. Consider:\n"; + echo " - Adding appropriate indexes\n"; + echo " - Limiting result sets with LIMIT\n"; + echo " - Breaking complex queries into smaller ones\n"; } echo "\n"; - // 8. Cleanup - echo "8. Cleanup:\n"; - $database->setQuery("DROP TABLE performance_test")->execute(); - echo "✓ Test table dropped\n"; + echo SEP; + echo "7. Cleanup:\n"; + $database->table('performance_test')->drop()->execute(); + echo " ✓ Test table dropped\n"; + } catch (Exception $e) { - echo "Error: ".$e->getMessage()."\n"; - echo "Stack trace:\n".$e->getTraceAsString()."\n"; + echo "✗ Error: ".$e->getMessage()."\n"; + try { + $database->table('performance_test')->drop(true)->execute(); + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ==="; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/09-multi-result-queries/example.php b/examples/09-multi-result-queries/example.php index 190d609..21eb883 100644 --- a/examples/09-multi-result-queries/example.php +++ b/examples/09-multi-result-queries/example.php @@ -2,136 +2,119 @@ require_once '../../vendor/autoload.php'; +use WebFiori\Database\ColOption; use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +use WebFiori\Database\DataType; use WebFiori\Database\MultiResultSet; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Multi-Result Queries Example ===\n\n"; try { - // Create connection - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); - echo "1. Setting up Test Data:\n"; - - // Create test tables - $database->raw(" - CREATE TABLE IF NOT EXISTS products ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - category VARCHAR(50) NOT NULL, - price DECIMAL(10,2) NOT NULL, - stock INT NOT NULL DEFAULT 0 - ) - ")->execute(); - - $database->raw(" - CREATE TABLE IF NOT EXISTS orders ( - id INT AUTO_INCREMENT PRIMARY KEY, - product_id INT, - quantity INT NOT NULL, - order_date DATE NOT NULL, - customer_name VARCHAR(100) NOT NULL - ) - ")->execute(); - - echo "✓ Test tables created\n"; - - // Clear existing data - $database->raw("DELETE FROM orders")->execute(); - $database->raw("DELETE FROM products")->execute(); - - // Insert sample products - $products = [ - ['Laptop', 'Electronics', 999.99, 10], - ['Mouse', 'Electronics', 29.99, 50], - ['Keyboard', 'Electronics', 79.99, 30], - ['Chair', 'Furniture', 199.99, 15], - ['Desk', 'Furniture', 299.99, 8], - ['Book', 'Education', 19.99, 100] - ]; - - foreach ($products as $product) { - $database->raw("INSERT INTO products (name, category, price, stock) VALUES (?, ?, ?, ?)", $product)->execute(); - } - - // Insert sample orders - $orders = [ - [1, 2, '2024-01-15', 'Ahmed Ali'], - [2, 5, '2024-01-16', 'Fatima Hassan'], - [3, 1, '2024-01-17', 'Omar Khalil'], - [1, 1, '2024-01-18', 'Layla Ahmed'], - [4, 3, '2024-01-19', 'Yusuf Ibrahim'] - ]; + echo SEP; + echo "1. Setting up Test Tables:\n"; + + $database->createBlueprint('products')->addColumns([ + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], + 'category' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 50], + 'price' => [ColOption::TYPE => DataType::DECIMAL, ColOption::SIZE => 10], + 'stock' => [ColOption::TYPE => DataType::INT, ColOption::DEFAULT => 0] + ]); + + $database->createBlueprint('orders')->addColumns([ + 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], + 'product-id' => [ColOption::TYPE => DataType::INT], + 'quantity' => [ColOption::TYPE => DataType::INT], + 'order-date' => [ColOption::TYPE => DataType::DATETIME], + 'customer-name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100] + ]); + + $database->table('orders')->drop(true)->execute(); + $database->table('products')->drop(true)->execute(); + $database->createTables(); + echo " ✓ Test tables created\n\n"; + + echo SEP; + echo "2. Inserting Sample Data:\n"; + + $database->table('products')->insert([ + 'cols' => ['name', 'category', 'price', 'stock'], + 'values' => [ + ['Laptop', 'Electronics', 999.99, 10], + ['Mouse', 'Electronics', 29.99, 50], + ['Keyboard', 'Electronics', 79.99, 30], + ['Chair', 'Furniture', 199.99, 15], + ['Desk', 'Furniture', 299.99, 8], + ['Book', 'Education', 19.99, 100] + ] + ])->execute(); + echo " ✓ 6 products inserted\n"; + + $database->table('orders')->insert([ + 'cols' => ['product-id', 'quantity', 'order-date', 'customer-name'], + 'values' => [ + [1, 2, '2024-01-15', 'Ahmed Ali'], + [2, 5, '2024-01-16', 'Fatima Hassan'], + [3, 1, '2024-01-17', 'Omar Khalil'], + [1, 1, '2024-01-18', 'Layla Ahmed'], + [4, 3, '2024-01-19', 'Yusuf Ibrahim'] + ] + ])->execute(); + echo " ✓ 5 orders inserted\n\n"; + + echo SEP; + echo "3. Creating Stored Procedure:\n"; - foreach ($orders as $order) { - $database->raw("INSERT INTO orders (product_id, quantity, order_date, customer_name) VALUES (?, ?, ?, ?)", $order)->execute(); - } - - echo "✓ Sample data inserted\n\n"; - - echo "2. Basic Multi-Result Query Example:\n"; - - // Create a stored procedure that returns multiple result sets $database->raw("DROP PROCEDURE IF EXISTS GetBusinessReport")->execute(); $database->raw(" CREATE PROCEDURE GetBusinessReport() BEGIN - -- Result Set 1: Product inventory SELECT 'Product Inventory' as report_section, name, category, price, stock - FROM products - ORDER BY category, name; + FROM products ORDER BY category, name; - -- Result Set 2: Sales summary by category - SELECT 'Sales by Category' as report_section, - p.category, - COUNT(o.id) as total_orders, - SUM(o.quantity) as total_quantity, + SELECT 'Sales by Category' as report_section, p.category, + COUNT(o.id) as total_orders, SUM(o.quantity) as total_quantity, SUM(o.quantity * p.price) as total_revenue - FROM products p - LEFT JOIN orders o ON p.id = o.product_id - GROUP BY p.category - ORDER BY total_revenue DESC; + FROM products p LEFT JOIN orders o ON p.id = o.product_id + GROUP BY p.category ORDER BY total_revenue DESC; - -- Result Set 3: Recent orders - SELECT 'Recent Orders' as report_section, - o.customer_name, - p.name as product_name, - o.quantity, - o.order_date, + SELECT 'Recent Orders' as report_section, o.customer_name, + p.name as product_name, o.quantity, o.order_date, (o.quantity * p.price) as order_total - FROM orders o - JOIN products p ON o.product_id = p.id - ORDER BY o.order_date DESC - LIMIT 10; + FROM orders o JOIN products p ON o.product_id = p.id + ORDER BY o.order_date DESC LIMIT 10; END ")->execute(); + echo " ✓ Stored procedure created\n\n"; + + echo SEP; + echo "4. Executing Multi-Result Query:\n"; - // Execute the multi-result procedure $result = $database->raw("CALL GetBusinessReport()")->execute(); if ($result instanceof MultiResultSet) { - echo "✓ Multi-result query executed successfully!\n"; - echo "Number of result sets: ".$result->count()."\n\n"; + echo " ✓ Multi-result query executed\n"; + echo " Number of result sets: ".$result->count()."\n\n"; - // Process each result set for ($i = 0; $i < $result->count(); $i++) { $resultSet = $result->getResultSet($i); - if ($resultSet->getRowsCount() > 0) { $firstRow = $resultSet->getRows()[0]; - if (isset($firstRow['report_section'])) { - echo "--- {$firstRow['report_section']} ---\n"; - + echo " --- {$firstRow['report_section']} ---\n"; foreach ($resultSet as $row) { if ($row['report_section'] === 'Product Inventory') { - echo " {$row['category']}: {$row['name']} - $".number_format($row['price'], 2)." (Stock: {$row['stock']})\n"; + echo " {$row['category']}: {$row['name']} - $".number_format($row['price'], 2)." (Stock: {$row['stock']})\n"; } elseif ($row['report_section'] === 'Sales by Category') { - echo " {$row['category']}: {$row['total_orders']} orders, {$row['total_quantity']} items, $".number_format($row['total_revenue'], 2)." revenue\n"; + echo " {$row['category']}: {$row['total_orders']} orders, $".number_format($row['total_revenue'], 2)." revenue\n"; } elseif ($row['report_section'] === 'Recent Orders') { - echo " {$row['customer_name']}: {$row['product_name']} x{$row['quantity']} = $".number_format($row['order_total'], 2)." ({$row['order_date']})\n"; + echo " {$row['customer_name']}: {$row['product_name']} x{$row['quantity']} = $".number_format($row['order_total'], 2)."\n"; } } echo "\n"; @@ -140,68 +123,40 @@ } } - echo "3. Advanced Multi-Result with Parameters:\n"; + echo SEP; + echo "5. Parameterized Multi-Result Query:\n"; - // Create a parameterized stored procedure $database->raw("DROP PROCEDURE IF EXISTS GetCategoryAnalysis")->execute(); $database->raw(" CREATE PROCEDURE GetCategoryAnalysis(IN category_filter VARCHAR(50)) BEGIN - -- Result Set 1: Products in category - SELECT 'Products in Category' as report_section, - name, price, stock - FROM products - WHERE category = category_filter - ORDER BY price DESC; - - -- Result Set 2: Category statistics - SELECT 'Category Statistics' as report_section, - category_filter as category, - COUNT(*) as product_count, - AVG(price) as avg_price, - MIN(price) as min_price, - MAX(price) as max_price, - SUM(stock) as total_stock; + SELECT 'Products' as section, name, price, stock + FROM products WHERE category COLLATE utf8mb4_unicode_520_ci = category_filter ORDER BY price DESC; - -- Result Set 3: Orders for this category - SELECT 'Category Orders' as report_section, - o.customer_name, - p.name as product_name, - o.quantity, - o.order_date - FROM orders o - JOIN products p ON o.product_id = p.id - WHERE p.category = category_filter - ORDER BY o.order_date DESC; + SELECT 'Statistics' as section, category_filter as category, + COUNT(*) as product_count, AVG(price) as avg_price, + MIN(price) as min_price, MAX(price) as max_price, SUM(stock) as total_stock + FROM products WHERE category COLLATE utf8mb4_unicode_520_ci = category_filter; END ")->execute(); - // Execute with parameter using raw() - $categoryResult = $database->raw("CALL GetCategoryAnalysis(?)", ['Electronics'])->execute(); + $categoryResult = $database->raw("CALL GetCategoryAnalysis('Electronics')")->execute(); if ($categoryResult instanceof MultiResultSet) { - echo "✓ Parameterized multi-result query executed for 'Electronics'!\n\n"; - + echo " ✓ Category analysis for 'Electronics':\n\n"; for ($i = 0; $i < $categoryResult->count(); $i++) { $rs = $categoryResult->getResultSet($i); - if ($rs->getRowsCount() > 0) { $firstRow = $rs->getRows()[0]; - - if (isset($firstRow['report_section'])) { - echo "--- {$firstRow['report_section']} ---\n"; - + if (isset($firstRow['section'])) { + echo " --- {$firstRow['section']} ---\n"; foreach ($rs as $row) { - if ($row['report_section'] === 'Products in Category') { - echo " {$row['name']}: $".number_format($row['price'], 2)." (Stock: {$row['stock']})\n"; - } elseif ($row['report_section'] === 'Category Statistics') { - echo " Category: {$row['category']}\n"; - echo " Product Count: {$row['product_count']}\n"; - echo " Average Price: $".number_format($row['avg_price'], 2)."\n"; - echo " Price Range: $".number_format($row['min_price'], 2)." - $".number_format($row['max_price'], 2)."\n"; - echo " Total Stock: {$row['total_stock']}\n"; - } elseif ($row['report_section'] === 'Category Orders') { - echo " {$row['customer_name']}: {$row['product_name']} x{$row['quantity']} ({$row['order_date']})\n"; + if ($row['section'] === 'Products') { + echo " {$row['name']}: $".number_format($row['price'], 2)." (Stock: {$row['stock']})\n"; + } elseif ($row['section'] === 'Statistics') { + echo " Product Count: {$row['product_count']}\n"; + echo " Average Price: $".number_format($row['avg_price'], 2)."\n"; + echo " Price Range: $".number_format($row['min_price'], 2)." - $".number_format($row['max_price'], 2)."\n"; } } echo "\n"; @@ -210,123 +165,47 @@ } } - echo "4. Working with Individual Result Sets:\n"; + echo SEP; + echo "6. Working with Individual Result Sets:\n"; - // Execute and work with specific result sets $businessReport = $database->raw("CALL GetBusinessReport()")->execute(); if ($businessReport instanceof MultiResultSet) { - // Get specific result sets $inventoryResults = $businessReport->getResultSet(0); $salesResults = $businessReport->getResultSet(1); $ordersResults = $businessReport->getResultSet(2); - echo "✓ Extracted individual result sets:\n"; - echo " - Inventory results: ".$inventoryResults->getRowsCount()." products\n"; - echo " - Sales results: ".$salesResults->getRowsCount()." categories\n"; - echo " - Orders results: ".$ordersResults->getRowsCount()." orders\n\n"; - - // Process specific result set - echo "Low stock products (< 20 items):\n"; + echo " ✓ Extracted individual result sets:\n"; + echo " - Inventory: ".$inventoryResults->getRowsCount()." products\n"; + echo " - Sales: ".$salesResults->getRowsCount()." categories\n"; + echo " - Orders: ".$ordersResults->getRowsCount()." orders\n\n"; + echo " Low stock products (< 20 items):\n"; foreach ($inventoryResults as $product) { if ($product['stock'] < 20) { - echo " ⚠️ {$product['name']}: {$product['stock']} remaining\n"; - } - } - echo "\n"; - - // Find best selling category - $bestCategory = null; - $bestRevenue = 0; - - foreach ($salesResults as $category) { - if ($category['total_revenue'] > $bestRevenue) { - $bestRevenue = $category['total_revenue']; - $bestCategory = $category['category']; + echo " ⚠️ {$product['name']}: {$product['stock']} remaining\n"; } } - - if ($bestCategory) { - echo "🏆 Best performing category: {$bestCategory} ($".number_format($bestRevenue, 2)." revenue)\n\n"; - } } + echo "\n"; - echo "5. Multi-Result with Complex Logic:\n"; - - // Create a procedure with conditional logic - $database->raw("DROP PROCEDURE IF EXISTS GetDynamicReport")->execute(); - $database->raw(" - CREATE PROCEDURE GetDynamicReport(IN report_type VARCHAR(20)) - BEGIN - IF report_type = 'summary' THEN - SELECT 'Summary Report' as report_section, - 'Products' as metric, COUNT(*) as value FROM products - UNION ALL - SELECT 'Summary Report' as report_section, - 'Orders' as metric, COUNT(*) as value FROM orders - UNION ALL - SELECT 'Summary Report' as report_section, - 'Categories' as metric, COUNT(DISTINCT category) as value FROM products; - - SELECT 'Revenue by Month' as report_section, - DATE_FORMAT(o.order_date, '%Y-%m') as month, - SUM(o.quantity * p.price) as revenue - FROM orders o - JOIN products p ON o.product_id = p.id - GROUP BY DATE_FORMAT(o.order_date, '%Y-%m') - ORDER BY month; - ELSE - SELECT 'Detailed Report' as report_section, - p.name, p.category, p.price, p.stock, - COALESCE(SUM(o.quantity), 0) as total_sold - FROM products p - LEFT JOIN orders o ON p.id = o.product_id - GROUP BY p.id, p.name, p.category, p.price, p.stock - ORDER BY total_sold DESC; - END IF; - END - ")->execute(); - - // Test with different parameters - echo "Summary Report:\n"; - $summaryResult = $database->raw("CALL GetDynamicReport(?)", ['summary'])->execute(); - - if ($summaryResult instanceof MultiResultSet) { - for ($i = 0; $i < $summaryResult->count(); $i++) { - $rs = $summaryResult->getResultSet($i); - - foreach ($rs as $row) { - if (isset($row['metric'])) { - echo " {$row['metric']}: {$row['value']}\n"; - } elseif (isset($row['month'])) { - echo " {$row['month']}: $".number_format($row['revenue'], 2)."\n"; - } - } - } - } - - echo "\n6. Cleanup:\n"; + echo SEP; + echo "7. Cleanup:\n"; $database->raw("DROP PROCEDURE IF EXISTS GetBusinessReport")->execute(); $database->raw("DROP PROCEDURE IF EXISTS GetCategoryAnalysis")->execute(); - $database->raw("DROP PROCEDURE IF EXISTS GetDynamicReport")->execute(); - $database->raw("DROP TABLE orders")->execute(); - $database->raw("DROP TABLE products")->execute(); - echo "✓ Test tables and procedures dropped\n"; + $database->table('orders')->drop()->execute(); + $database->table('products')->drop()->execute(); + echo " ✓ Tables and procedures dropped\n"; + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - echo "Stack trace:\n".$e->getTraceAsString()."\n"; - - // Clean up on error try { $database->raw("DROP PROCEDURE IF EXISTS GetBusinessReport")->execute(); $database->raw("DROP PROCEDURE IF EXISTS GetCategoryAnalysis")->execute(); - $database->raw("DROP PROCEDURE IF EXISTS GetDynamicReport")->execute(); - $database->raw("DROP TABLE IF EXISTS orders")->execute(); - $database->raw("DROP TABLE IF EXISTS products")->execute(); - } catch (Exception $cleanupError) { - // Ignore cleanup errors - } + $database->table('orders')->drop(true)->execute(); + $database->table('products')->drop(true)->execute(); + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/10-attribute-based-tables/example.php b/examples/10-attribute-based-tables/example.php index 18c5987..dc389b9 100644 --- a/examples/10-attribute-based-tables/example.php +++ b/examples/10-attribute-based-tables/example.php @@ -8,52 +8,55 @@ use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Attribute-Based Tables Example ===\n\n"; try { - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Building Tables from Attributes:\n"; $authorsTable = AttributeTableBuilder::build(Author::class, 'mysql'); $articlesTable = AttributeTableBuilder::build(Article::class, 'mysql'); - echo "✓ Authors table blueprint created\n"; - echo " Columns: ".implode(', ', array_keys($authorsTable->getCols()))."\n"; + echo " ✓ Authors table blueprint created\n"; + echo " Columns: ".implode(', ', array_keys($authorsTable->getCols()))."\n"; - echo "✓ Articles table blueprint created\n"; - echo " Columns: ".implode(', ', array_keys($articlesTable->getCols()))."\n\n"; + echo " ✓ Articles table blueprint created\n"; + echo " Columns: ".implode(', ', array_keys($articlesTable->getCols()))."\n\n"; + echo SEP; echo "2. Generated SQL:\n"; - echo "Authors table:\n".$authorsTable->toSQL()."\n\n"; - echo "Articles table:\n".$articlesTable->toSQL()."\n\n"; - - echo "3. Creating Tables in Database:\n"; + echo " Authors table:\n ".$authorsTable->toSQL()."\n\n"; + echo " Articles table:\n ".$articlesTable->toSQL()."\n\n"; - $database->raw("DROP TABLE IF EXISTS articles")->execute(); - $database->raw("DROP TABLE IF EXISTS authors")->execute(); + echo SEP; + echo "3. Creating Tables:\n"; - $database->raw($authorsTable->toSQL())->execute(); - echo "✓ Authors table created\n"; + $database->addTable($authorsTable); + $database->addTable($articlesTable); - $database->raw($articlesTable->toSQL())->execute(); - echo "✓ Articles table created\n\n"; + $database->table('articles')->drop(true)->execute(); + $database->table('authors')->drop(true)->execute(); + $database->createTables(); + echo " ✓ Tables created\n\n"; + echo SEP; echo "4. Inserting Test Data:\n"; - $database->addTable($authorsTable); - $database->addTable($articlesTable); - $database->table('authors')->insert(['name' => 'Ibrahim Ali', 'email' => 'ibrahim@example.com'])->execute(); $database->table('authors')->insert(['name' => 'Sara Ahmed', 'email' => 'sara@example.com'])->execute(); - echo "✓ Authors inserted\n"; + echo " ✓ 2 authors inserted\n"; $database->table('articles')->insert(['author-id' => 1, 'title' => 'Introduction to PHP 8 Attributes', 'content' => 'PHP 8 introduced attributes...'])->execute(); $database->table('articles')->insert(['author-id' => 1, 'title' => 'Database Design Patterns', 'content' => 'Learn about patterns...'])->execute(); $database->table('articles')->insert(['author-id' => 2, 'title' => 'Clean Architecture in PHP', 'content' => 'Implementing clean architecture...'])->execute(); - echo "✓ Articles inserted\n\n"; + echo " ✓ 3 articles inserted\n\n"; + echo SEP; echo "5. Querying Data:\n"; $result = $database->raw(" @@ -62,22 +65,25 @@ ORDER BY ar.`published-at` DESC ")->execute(); - echo "Articles with authors:\n"; + echo " Articles with authors:\n"; foreach ($result as $row) { - echo " - {$row['title']} by {$row['author']} ({$row['published-at']})\n"; + echo " - {$row['title']} by {$row['author']} ({$row['published-at']})\n"; } echo "\n"; + echo SEP; echo "6. Cleanup:\n"; - $database->raw("DROP TABLE articles")->execute(); - $database->raw("DROP TABLE authors")->execute(); - echo "✓ Tables dropped\n"; + $database->table('articles')->drop()->execute(); + $database->table('authors')->drop()->execute(); + echo " ✓ Tables dropped\n"; + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; try { - $database->raw("DROP TABLE IF EXISTS articles")->execute(); - $database->raw("DROP TABLE IF EXISTS authors")->execute(); + $database->table('articles')->drop(true)->execute(); + $database->table('authors')->drop(true)->execute(); } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/11-repository-pattern/example.php b/examples/11-repository-pattern/example.php index 42e3b68..83ff2a0 100644 --- a/examples/11-repository-pattern/example.php +++ b/examples/11-repository-pattern/example.php @@ -9,15 +9,17 @@ use WebFiori\Database\Database; use WebFiori\Database\DataType; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Repository Pattern Example ===\n\n"; try { - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Setting up Database:\n"; - $database->raw("DROP TABLE IF EXISTS products")->execute(); $database->createBlueprint('products')->addColumns([ 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], @@ -25,14 +27,17 @@ 'price' => [ColOption::TYPE => DataType::DECIMAL, ColOption::SIZE => 10], 'stock' => [ColOption::TYPE => DataType::INT] ]); - $database->table('products')->createTable(); - $database->execute(); - echo "✓ Products table created\n\n"; + $database->table('products')->drop(true)->execute(); + $database->table('products')->createTable()->execute(); + echo " ✓ Products table created\n\n"; + + echo SEP; echo "2. Creating Repository:\n"; $productRepo = new ProductRepository($database); - echo "✓ ProductRepository created\n\n"; + echo " ✓ ProductRepository created\n\n"; + echo SEP; echo "3. Saving Products (Create):\n"; $products = [ new Product('Laptop', 'Electronics', 999.99, 15), @@ -45,74 +50,87 @@ foreach ($products as $product) { $productRepo->save($product); - echo " ✓ Saved: {$product->name}\n"; + echo " ✓ Saved: {$product->name}\n"; } echo "\n"; + echo SEP; echo "4. Finding All Products (Read):\n"; $allProducts = $productRepo->findAll(); - echo "Total products: ".count($allProducts)."\n"; + echo " Total products: ".count($allProducts)."\n"; foreach ($allProducts as $p) { - echo " - {$p->name} ({$p->category}): \${$p->price} - Stock: {$p->stock}\n"; + echo " - {$p->name} ({$p->category}): \${$p->price} - Stock: {$p->stock}\n"; } echo "\n"; + echo SEP; echo "5. Finding by ID:\n"; $product = $productRepo->findById(1); if ($product) { - echo " Found: {$product->name} - \${$product->price}\n\n"; + echo " Found: {$product->name} - \${$product->price}\n\n"; } + echo SEP; echo "6. Custom Query - Find by Category:\n"; $electronics = $productRepo->findByCategory('Electronics'); - echo "Electronics products: ".count($electronics)."\n"; + echo " Electronics products: ".count($electronics)."\n"; foreach ($electronics as $p) { - echo " - {$p->name}: \${$p->price}\n"; + echo " - {$p->name}: \${$p->price}\n"; } echo "\n"; + echo SEP; echo "7. Custom Query - Find Low Stock:\n"; $lowStock = $productRepo->findLowStock(10); - echo "Low stock products (< 10): ".count($lowStock)."\n"; + echo " Low stock products (< 10): ".count($lowStock)."\n"; foreach ($lowStock as $p) { - echo " ⚠️ {$p->name}: {$p->stock} remaining\n"; + echo " ⚠️ {$p->name}: {$p->stock} remaining\n"; } echo "\n"; + echo SEP; echo "8. Updating a Product:\n"; $product = $productRepo->findById(3); if ($product) { - echo " Before: {$product->name} - Stock: {$product->stock}\n"; + echo " Before: {$product->name} - Stock: {$product->stock}\n"; $product->stock = 25; $productRepo->save($product); $updated = $productRepo->findById(3); - echo " After: {$updated->name} - Stock: {$updated->stock}\n\n"; + echo " After: {$updated->name} - Stock: {$updated->stock}\n\n"; } + echo SEP; echo "9. Pagination (Offset-based):\n"; $page1 = $productRepo->paginate(1, 3); - echo "Page 1 (3 per page):\n"; - echo " Total items: {$page1->getTotalItems()}\n"; - echo " Total pages: {$page1->getTotalPages()}\n"; + echo " Page 1 (3 per page):\n"; + echo " Total items: {$page1->getTotalItems()}\n"; + echo " Total pages: {$page1->getTotalPages()}\n"; foreach ($page1->getItems() as $p) { - echo " - {$p->name}\n"; + echo " - {$p->name}\n"; } echo "\n"; + echo SEP; echo "10. Counting Products:\n"; - echo " Total products in database: ".$productRepo->count()."\n\n"; + echo " Total products in database: ".$productRepo->count()."\n\n"; + echo SEP; echo "11. Deleting a Product:\n"; $productRepo->deleteById(6); - echo " ✓ Deleted product with ID 6\n"; - echo " Products remaining: ".$productRepo->count()."\n\n"; + echo " ✓ Deleted product with ID 6\n"; + echo " Products remaining: ".$productRepo->count()."\n\n"; + echo SEP; echo "12. Cleanup:\n"; - $database->raw("DROP TABLE products")->execute(); - echo "✓ Products table dropped\n"; + $database->table('products')->drop()->execute(); + echo " ✓ Products table dropped\n"; + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - try { $database->raw("DROP TABLE IF EXISTS products")->execute(); } catch (Exception $cleanupError) {} + try { + $database->table('products')->drop(true)->execute(); + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/12-clean-architecture/example.php b/examples/12-clean-architecture/example.php index 31bd30a..922e80b 100644 --- a/examples/12-clean-architecture/example.php +++ b/examples/12-clean-architecture/example.php @@ -12,6 +12,8 @@ use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Clean Architecture Example ===\n\n"; echo "Architecture layers:\n"; @@ -20,26 +22,29 @@ echo " - Infrastructure/Repository: Data access with AbstractRepository\n\n"; try { - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Building Table from Attributes:\n"; - // Build table from attribute-based class $table = AttributeTableBuilder::build(UserTable::class, 'mysql'); - echo "✓ Table blueprint built from UserTable attributes\n"; - echo " Columns: ".implode(', ', array_keys($table->getCols()))."\n\n"; + echo " ✓ Table blueprint built from UserTable attributes\n"; + echo " Columns: ".implode(', ', array_keys($table->getCols()))."\n\n"; + echo SEP; echo "2. Creating Table:\n"; - $database->raw("DROP TABLE IF EXISTS users")->execute(); $database->addTable($table); - $database->createTables()->execute(); - echo "✓ Users table created\n\n"; + $database->table('users')->drop(true)->execute(); + $database->createTables(); + echo " ✓ Users table created\n\n"; + echo SEP; echo "3. Using Repository (extends AbstractRepository):\n"; $userRepo = new UserRepository($database); - echo "✓ UserRepository created\n\n"; + echo " ✓ UserRepository created\n\n"; + echo SEP; echo "4. Saving Domain Entities:\n"; $users = [ new User(null, 'Ahmed Ali', 'ahmed@example.com', 28), @@ -49,43 +54,44 @@ foreach ($users as $user) { $userRepo->save($user); - echo " ✓ Saved: {$user->name}\n"; + echo " ✓ Saved: {$user->name}\n"; } echo "\n"; + echo SEP; echo "5. Repository Operations:\n"; - // findAll() $all = $userRepo->findAll(); - echo "All users (".count($all)."):\n"; + echo " All users (".count($all)."):\n"; foreach ($all as $u) { - echo " - {$u->name} ({$u->email}) - Age: {$u->age}\n"; + echo " - {$u->name} ({$u->email}) - Age: {$u->age}\n"; } - // findById() $user = $userRepo->findById(1); - echo "\nFind by ID 1: {$user->name}\n"; + echo "\n Find by ID 1: {$user->name}\n"; - // Custom method $adults = $userRepo->findByAge(25); - echo "\nUsers age >= 25 (".count($adults)."):\n"; + echo "\n Users age >= 25 (".count($adults)."):\n"; foreach ($adults as $u) { - echo " - {$u->name} (Age: {$u->age})\n"; + echo " - {$u->name} (Age: {$u->age})\n"; } - // Pagination $page = $userRepo->paginate(1, 2); - echo "\nPage 1 (2 per page): {$page->getTotalItems()} total, {$page->getTotalPages()} pages\n"; + echo "\n Page 1 (2 per page): {$page->getTotalItems()} total, {$page->getTotalPages()} pages\n"; - // count() - echo "\nTotal count: ".$userRepo->count()."\n\n"; + echo "\n Total count: ".$userRepo->count()."\n\n"; + echo SEP; echo "6. Cleanup:\n"; - $database->raw("DROP TABLE users")->execute(); - echo "✓ Table dropped\n"; + $database->table('users')->drop()->execute(); + echo " ✓ Table dropped\n"; + } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - try { $database->raw("DROP TABLE IF EXISTS users")->execute(); } catch (Exception $e) {} + try { + $database->table('users')->drop(true)->execute(); + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/13-pagination/example.php b/examples/13-pagination/example.php index 919d3a3..706815a 100644 --- a/examples/13-pagination/example.php +++ b/examples/13-pagination/example.php @@ -9,86 +9,100 @@ use WebFiori\Database\Database; use WebFiori\Database\DataType; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== WebFiori Database Pagination Example ===\n\n"; try { - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Setting up Test Data:\n"; - $database->raw("DROP TABLE IF EXISTS users")->execute(); $database->createBlueprint('users')->addColumns([ 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true], 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100], 'email' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 150], 'age' => [ColOption::TYPE => DataType::INT] ]); - $database->table('users')->createTable(); - $database->execute(); + + $database->table('users')->drop(true)->execute(); + $database->table('users')->createTable()->execute(); $names = ['Ahmed', 'Fatima', 'Omar', 'Layla', 'Hassan', 'Sara', 'Yusuf', 'Maryam', 'Ali', 'Noor', 'Khalid', 'Aisha', 'Ibrahim', 'Zahra', 'Mahmoud', 'Hana', 'Tariq', 'Salma', 'Rami', 'Dina', 'Faisal', 'Lina', 'Samir', 'Rania', 'Walid']; + $values = []; foreach ($names as $i => $name) { - $database->table('users')->insert([ - 'name' => $name, - 'email' => strtolower($name).'@example.com', - 'age' => 20 + ($i % 30) - ])->execute(); + $values[] = [$name, strtolower($name).'@example.com', 20 + ($i % 30)]; } - echo "✓ Created 25 test users\n\n"; + + $database->table('users')->insert([ + 'cols' => ['name', 'email', 'age'], + 'values' => $values + ])->execute(); + echo " ✓ Created 25 test users\n\n"; $repo = new UserRepository($database); + echo SEP; echo "2. Offset-Based Pagination:\n"; echo " (Traditional page numbers)\n\n"; for ($page = 1; $page <= 3; $page++) { $result = $repo->paginate($page, 5); - echo "Page $page of {$result->getTotalPages()}:\n"; + echo " Page $page of {$result->getTotalPages()}:\n"; foreach ($result->getItems() as $user) { - echo " - {$user->name} ({$user->email})\n"; + echo " - {$user->name} ({$user->email})\n"; } - echo " Has next: ".($result->hasNextPage() ? 'Yes' : 'No')."\n\n"; + echo " Has next: ".($result->hasNextPage() ? 'Yes' : 'No')."\n\n"; } + echo SEP; echo "3. Cursor-Based Pagination:\n"; echo " (Better for large datasets, infinite scroll)\n\n"; - $cursor = null; // null = start from beginning (first page) + $cursor = null; $pageNum = 1; while ($pageNum <= 3) { $result = $repo->paginateByCursor($cursor, 5, 'id', 'ASC'); - echo "Cursor Page $pageNum:\n"; + echo " Cursor Page $pageNum:\n"; foreach ($result->getItems() as $user) { - echo " - ID {$user->id}: {$user->name}\n"; + echo " - ID {$user->id}: {$user->name}\n"; } - echo " Has more: ".($result->hasMore() ? 'Yes' : 'No')."\n"; + echo " Has more: ".($result->hasMore() ? 'Yes' : 'No')."\n"; if (!$result->hasMore()) break; - // Next cursor is base64-encoded ID of last item, used to fetch next page $cursor = $result->getNextCursor(); - echo " Next cursor: $cursor\n\n"; + echo " Next cursor: $cursor\n\n"; $pageNum++; } + echo "\n"; - echo "\n4. Pagination with Ordering:\n"; + echo SEP; + echo "4. Pagination with Ordering:\n"; $result = $repo->paginate(1, 5, ['age' => 'DESC']); - echo "Top 5 oldest users:\n"; + echo " Top 5 oldest users:\n"; foreach ($result->getItems() as $user) { - echo " - {$user->name} (Age: {$user->age})\n"; + echo " - {$user->name} (Age: {$user->age})\n"; } + echo "\n"; + + echo SEP; + echo "5. Cleanup:\n"; + $database->table('users')->drop()->execute(); + echo " ✓ Table dropped\n"; - echo "\n5. Cleanup:\n"; - $database->raw("DROP TABLE users")->execute(); - echo "✓ Table dropped\n"; } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; - try { $database->raw("DROP TABLE IF EXISTS users")->execute(); } catch (Exception $e) {} + try { + $database->table('users')->drop(true)->execute(); + } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/14-active-record-model/example.php b/examples/14-active-record-model/example.php index 96ba72f..7894c5d 100644 --- a/examples/14-active-record-model/example.php +++ b/examples/14-active-record-model/example.php @@ -7,29 +7,32 @@ use WebFiori\Database\ConnectionInfo; use WebFiori\Database\Database; +const SEP = "────────────────────────────────────────────────────────────────────\n"; + echo "=== Active Record Model Example ===\n\n"; echo "This example shows Entity + Repository merged into a single Model class.\n\n"; try { - $connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql'); + $connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db'); $database = new Database($connection); + echo SEP; echo "1. Creating Table from Model Attributes:\n"; - // Build table from Article class attributes $table = AttributeTableBuilder::build(Article::class, 'mysql'); $database->addTable($table); + $database->table('articles')->drop(true)->execute(); $database->table('articles')->createTable()->execute(); - echo "✓ Articles table created from class attributes\n\n"; + echo " ✓ Articles table created from class attributes\n\n"; + echo SEP; echo "2. Using the Model:\n"; - // Create and save articles directly $article1 = new Article($database); $article1->title = 'Introduction to WebFiori'; $article1->content = 'WebFiori is a PHP framework...'; $article1->authorName = 'Ahmad Hassan'; - $article1->save(); // Saves itself + $article1->save(); $article2 = new Article($database); $article2->title = 'Database Patterns'; @@ -43,57 +46,56 @@ $article3->authorName = 'Ahmad Hassan'; $article3->save(); - echo "✓ Articles saved\n\n"; + echo " ✓ 3 articles saved\n\n"; + echo SEP; echo "3. Querying with Repository Methods:\n"; - // Use any instance for queries $articleModel = new Article($database); - // findAll() $all = $articleModel->findAll(); - echo "All articles ({$articleModel->count()}):\n"; + echo " All articles ({$articleModel->count()}):\n"; foreach ($all as $article) { - echo " - {$article->title} by {$article->authorName}\n"; + echo " - {$article->title} by {$article->authorName}\n"; } echo "\n"; - // Custom query method $byAuthor = $articleModel->findByAuthor('Ahmad Hassan'); - echo "Articles by Ahmad Hassan:\n"; + echo " Articles by Ahmad Hassan:\n"; foreach ($byAuthor as $article) { - echo " - {$article->title}\n"; + echo " - {$article->title}\n"; } echo "\n"; + echo SEP; echo "4. Update and Delete:\n"; - // Update $first = $articleModel->findById(1); $first->title = 'Updated: Introduction to WebFiori'; - $first->save(); // Saves itself - echo "✓ Article updated\n"; + $first->save(); + echo " ✓ Article updated\n"; - // Delete $first->id = 2; $first->deleteById(); - echo "✓ Article deleted\n"; + echo " ✓ Article deleted\n"; - echo "\nRemaining articles:\n"; + echo "\n Remaining articles:\n"; foreach ($articleModel->findAll() as $article) { - echo " - [{$article->id}] {$article->title}\n"; + echo " - [{$article->id}] {$article->title}\n"; } echo "\n"; + echo SEP; echo "5. Cleanup:\n"; - $database->raw("DROP TABLE articles")->execute(); - echo "✓ Table dropped\n"; + $database->table('articles')->drop()->execute(); + echo " ✓ Table dropped\n"; } catch (Exception $e) { echo "✗ Error: ".$e->getMessage()."\n"; try { - $database->raw("DROP TABLE IF EXISTS articles")->execute(); + $database->table('articles')->drop(true)->execute(); } catch (Exception $cleanupError) {} } -echo "\n=== Example Complete ===\n"; +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/15-eager-loading/Author.php b/examples/15-eager-loading/Author.php new file mode 100644 index 0000000..225195b --- /dev/null +++ b/examples/15-eager-loading/Author.php @@ -0,0 +1,9 @@ +id = (int) $row['id']; + $author->name = $row['name']; + return $author; + } + + protected function toArray(object $entity): array { + return [ + 'id' => $entity->id, + 'name' => $entity->name + ]; + } +} diff --git a/examples/15-eager-loading/AuthorsTable.php b/examples/15-eager-loading/AuthorsTable.php new file mode 100644 index 0000000..b6da9ea --- /dev/null +++ b/examples/15-eager-loading/AuthorsTable.php @@ -0,0 +1,19 @@ +id = (int) $row['id']; + $comment->content = $row['content']; + $comment->postId = (int) ($row['post-id'] ?? $row['post_id'] ?? 0); + return $comment; + } + + protected function toArray(object $entity): array { + return [ + 'id' => $entity->id, + 'content' => $entity->content, + 'post-id' => $entity->postId + ]; + } +} diff --git a/examples/15-eager-loading/CommentsTable.php b/examples/15-eager-loading/CommentsTable.php new file mode 100644 index 0000000..dab1c53 --- /dev/null +++ b/examples/15-eager-loading/CommentsTable.php @@ -0,0 +1,22 @@ +id = (int) $row['id']; + $post->title = $row['title']; + $post->authorId = (int) ($row['author-id'] ?? $row['author_id'] ?? 0); + return $post; + } + + protected function toArray(object $entity): array { + return [ + 'id' => $entity->id, + 'title' => $entity->title, + 'author-id' => $entity->authorId + ]; + } +} diff --git a/examples/15-eager-loading/PostsTable.php b/examples/15-eager-loading/PostsTable.php new file mode 100644 index 0000000..aaf2735 --- /dev/null +++ b/examples/15-eager-loading/PostsTable.php @@ -0,0 +1,24 @@ +findAll(); // 1 query + +foreach ($authors as $author) { + $posts = $postRepo->findByAuthorId($author->id); // N queries! +} +// Total: 1 + N queries (bad!) +``` + +## Solution: Eager Loading + +With eager loading, related data is fetched in batches: + +```php +$authors = $authorRepo->with(['posts'])->findAll(); +// Query 1: SELECT * FROM authors +// Query 2: SELECT * FROM posts WHERE author-id IN (1, 2, 3, ...) +// Total: 2 queries (good!) + +foreach ($authors as $author) { + // $author->posts is already populated! + foreach ($author->posts as $post) { + echo $post->title; + } +} +``` + +## Architecture (Clean Separation) + +``` +Domain/ # Pure entities - no DB knowledge +├── Author.php +└── Post.php + +Infrastructure/ # Database concerns +├── AuthorsTable.php # Table definition + relationships +├── PostsTable.php +├── AuthorRepository.php # Links table to entity +└── PostRepository.php +``` + +## Defining Relationships + +### HasMany (One-to-Many) + +```php +#[Table(name: 'authors')] +#[HasMany( + entity: Post::class, + foreignKey: 'author-id', + property: 'posts', + table: 'posts' +)] +class AuthorsTable {} +``` + +### BelongsTo (Many-to-One) + +```php +#[Table(name: 'posts')] +class PostsTable { + #[Column(name: 'author-id', type: DataType::INT)] + #[ForeignKey(table: AuthorsTable::class, column: 'id', property: 'author')] + public int $authorId; +} +``` + +## Repository Setup + +```php +class AuthorRepository extends AbstractRepository { + // Link to table definition for relationship discovery + protected function getTableClass(): string { + return AuthorsTable::class; + } + + // Map related rows to entities + protected function relatedToEntity(string $relation, array $row): object { + if ($relation === 'posts') { + $post = new Post(); + $post->id = (int) $row['id']; + $post->title = $row['title']; + return $post; + } + return (object) $row; + } +} +``` + +## Usage + +```php +// HasMany: Author with their posts +$authors = $authorRepo->with(['posts'])->findAll(); + +// BelongsTo: Posts with their author +$posts = $postRepo->with(['author'])->findAll(); + +// Works with findById +$author = $authorRepo->with(['posts'])->findById(1); + +// Works with pagination +$page = $authorRepo->with(['posts'])->paginate(1, 20); +``` + +## Files + +- [`example.php`](example.php) - Main example demonstrating eager loading +- [`Author.php`](Author.php) - Domain entity +- [`Post.php`](Post.php) - Domain entity +- [`AuthorsTable.php`](AuthorsTable.php) - Table definition with HasMany +- [`PostsTable.php`](PostsTable.php) - Table definition with BelongsTo +- [`AuthorRepository.php`](AuthorRepository.php) - Repository implementation +- [`PostRepository.php`](PostRepository.php) - Repository implementation + +## Running the Example + +```bash +php example.php +``` + +## Query Comparison + +| Approach | Authors | Posts per Author | Total Queries | +|----------|---------|------------------|---------------| +| N+1 (bad) | 100 | any | 101 | +| Eager loading | 100 | any | 2 | + +## Related Examples + +- [11-repository-pattern](../11-repository-pattern/) - Basic repository usage +- [12-clean-architecture](../12-clean-architecture/) - Domain/Infrastructure separation +- [13-pagination](../13-pagination/) - Pagination techniques diff --git a/examples/15-eager-loading/example.php b/examples/15-eager-loading/example.php new file mode 100644 index 0000000..8a3c81f --- /dev/null +++ b/examples/15-eager-loading/example.php @@ -0,0 +1,224 @@ +addTable(AttributeTableBuilder::build(AuthorsTable::class, 'mysql')); + $database->addTable(AttributeTableBuilder::build(PostsTable::class, 'mysql')); + $database->addTable(AttributeTableBuilder::build(CommentsTable::class, 'mysql')); + + $database->table('comments')->drop(true)->execute(); + $database->table('posts')->drop(true)->execute(); + $database->table('authors')->drop(true)->execute(); + + $database->createTables(); + echo "✓ Tables created\n\n"; + + // Seed data + echo SEP; + echo "2. Seeding Data:\n"; + $database->table('authors')->insert([ + 'cols' => [ + 'name' + ], + 'values' => [ + ['Ahmad Hassan'], + ['Fatima Ali'], + ['Omar Khalid'] + ] + ])->execute(); + + $database->table('posts')->insert([ + 'cols' => [ + 'title', 'author-id' + ], + 'values' => [ + ['Introduction to PHP', 1], + ['Database Design', 1], + ['Clean Architecture', 1], + ['Web Security', 2], + ['API Design', 2] + ] + ])->execute(); + + echo "✓ 3 authors and 5 posts created\n\n"; + + $authorRepo = new AuthorRepository($database); + $postRepo = new PostRepository($database); + + // ============================================= + // WITHOUT Eager Loading (N+1 Problem) + // ============================================= + echo SEP; + echo "3. WITHOUT Eager Loading (N+1 Problem):\n"; + echo " If we fetched posts for each author separately:\n"; + echo " - 1 query to get all authors\n"; + echo " - N queries to get posts for each author (3 more queries)\n"; + echo " - Total: 4 queries for 3 authors\n\n"; + + // ============================================= + // WITH Eager Loading - HasMany + // ============================================= + echo SEP; + echo "4. WITH Eager Loading - HasMany (Author -> Posts):\n"; + echo " Using: \$authorRepo->with(['posts'])->findAll()\n\n"; + + $authors = $authorRepo->with(['posts'])->findAll(); + + foreach ($authors as $author) { + echo " {$author->name} ({$author->id}):\n"; + if (empty($author->posts)) { + echo " - No posts\n"; + } else { + foreach ($author->posts as $post) { + echo " - {$post->title}\n"; + } + } + } + echo "\n ✓ Only 2 queries executed (1 for authors + 1 for all posts)\n\n"; + + // ============================================= + // WITH Eager Loading - String syntax + // ============================================= + echo SEP; + echo "5. WITH Eager Loading - String syntax (single relation):\n"; + echo " Using: \$postRepo->with('author')->findAll()\n\n"; + + $posts = $postRepo->with('author')->findAll(); + + foreach ($posts as $post) { + echo " \"{$post->title}\" by {$post->author->name}\n"; + } + echo "\n ✓ String syntax works for single relation\n\n"; + + // ============================================= + // WITH Eager Loading - Multiple relations + // ============================================= + echo SEP; + echo "6. WITH Eager Loading - Multiple relations:\n"; + echo " Using: \$postRepo->with(['author', 'comments'])->findAll()\n\n"; + + $posts = $postRepo->with(['author', 'comments'])->findAll(); + + foreach ($posts as $post) { + $commentCount = count($post->comments); + echo " \"{$post->title}\" by {$post->author->name} - {$commentCount} comments\n"; + } + echo "\n ✓ 3 queries executed (1 for posts + 1 for authors + 1 for comments)\n\n"; + + // ============================================= + // WITH JOIN Loading - BelongsTo (single query) + // ============================================= + echo SEP; + echo "7. WITH JOIN Loading - BelongsTo using withJoin() (1 query):\n"; + echo " Using: \$postRepo->withJoin('author')->findAll()\n\n"; + + $posts = $postRepo->withJoin('author')->findAll(); + + foreach ($posts as $post) { + echo " \"{$post->title}\" by {$post->author->name}\n"; + } + echo "\n ✓ Only 1 query executed (JOIN)\n\n"; + + // ============================================= + // withJoin() prevents hasMany (cartesian product protection) + // ============================================= + echo SEP; + echo "8. withJoin() prevents hasMany (cartesian product protection):\n"; + try { + $authorRepo->withJoin('posts')->findAll(); + echo " ✗ Should have thrown exception\n"; + } catch (\WebFiori\Database\Repository\RepositoryException $e) { + echo " ✓ Exception thrown: " . $e->getMessage() . "\n"; + } + echo "\n"; + + // ============================================= + // Eager Loading with findById + // ============================================= + echo SEP; + echo "9. Eager Loading with findById:\n"; + + $author = $authorRepo->with('posts')->findById(1); + echo " Author: {$author->name}\n"; + echo " Posts: " . count($author->posts) . "\n\n"; + + // ============================================= + // Eager Loading with Pagination + // ============================================= + echo SEP; + echo "10. Eager Loading with Pagination:\n"; + + $page = $authorRepo->with('posts')->paginate(page: 1, perPage: 2); + echo " Page 1 of " . $page->getTotalPages() . " (2 per page):\n"; + foreach ($page->getItems() as $author) { + echo " - {$author->name}: " . count($author->posts) . " posts\n"; + } + echo "\n"; + + // ============================================= + // stdClass Relationship (no entity specified) + // ============================================= + echo SEP; + echo "11. stdClass Relationship (no entity specified):\n"; + + $database->table('comments')->insert([ + 'cols' => ['content', 'post-id'], + 'values' => [ + ['Great article!', 1], + ['Very helpful', 1], + ['Thanks for sharing', 2] + ] + ])->execute(); + + $commentRepo = new CommentRepository($database); + $comments = $commentRepo->with('post')->findAll(); + + echo " Comments with related posts (stdClass):\n"; + foreach ($comments as $comment) { + $postTitle = $comment->post->title ?? 'N/A'; + echo " - \"{$comment->content}\" on post: \"{$postTitle}\"\n"; + echo " post type: " . get_class($comment->post) . "\n"; + } + echo "\n"; + + // Cleanup + echo SEP; + echo "12. Cleanup:\n"; + $database->table('comments')->drop()->execute(); + $database->table('posts')->drop()->execute(); + $database->table('authors')->drop()->execute(); + echo "✓ Tables dropped\n"; + +} catch (Exception $e) { + echo "✗ Error: " . $e->getMessage() . "\n"; + try { + $database->table('comments')->drop(true)->execute(); + $database->table('posts')->drop(true)->execute(); + $database->table('authors')->drop(true)->execute(); + } catch (Exception $cleanupError) {} +} + +echo "\n" . SEP; +echo "=== Example Complete ===\n"; diff --git a/examples/README.md b/examples/README.md index e82bcdc..5cf51ad 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,6 +20,7 @@ This directory contains practical examples demonstrating how to use the WebFiori | 12 | [clean-architecture](12-clean-architecture/) | Clean architecture with domain/infrastructure separation | | 13 | [pagination](13-pagination/) | Offset and cursor-based pagination | | 14 | [active-record-model](14-active-record-model/) | Entity + Repository merged into single Model class | +| 15 | [eager-loading](15-eager-loading/) | Avoid N+1 queries with relationship eager loading | ## Prerequisites @@ -133,3 +134,9 @@ You can modify the connection parameters in each example as needed. - Using attributes to define table structure on the model - Active Record pattern for simpler projects - Trade-offs vs Repository pattern + +### 15-eager-loading +- Avoiding N+1 query problem with `with()` method +- HasMany relationships (one-to-many) +- BelongsTo relationships (many-to-one) +- Eager loading with pagination diff --git a/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php b/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php index 4b1ad9c..b349f94 100644 --- a/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php +++ b/tests/WebFiori/Tests/Database/MsSql/MSSQLQueryBuilderTest.php @@ -399,7 +399,11 @@ public function testWhereIn02() { */ public function testCreateTables() { $schema = new MSSQLTestSchema(); - $schema->createTables(); + + // Verify each table's SQL individually since createTables() now executes directly + $tables = $schema->getTables(); + $this->assertCount(4, $tables); + $this->assertEquals("if not exists (select * from sysobjects where name='users' and xtype='U')\n" . "create table [users] (\n" . " [id] [int] identity(1,1) not null,\n" @@ -407,9 +411,9 @@ public function testCreateTables() { . " [last_name] [nvarchar](20) not null,\n" . " [age] [int] not null,\n" . " constraint users_pk primary key clustered([id]) on [PRIMARY]\n" - . ")\n" - . "\n" - . "if not exists (select * from sysobjects where name='users_privileges' and xtype='U')\n" + . ")\n", $schema->getTable('users')->toSQL()); + + $this->assertEquals("if not exists (select * from sysobjects where name='users_privileges' and xtype='U')\n" . "create table [users_privileges] (\n" . " [id] [int] not null,\n" . " [can_edit_price] [bit] not null default 0,\n" @@ -417,9 +421,9 @@ public function testCreateTables() { . " [can_do_anything] [bit] not null,\n" . " constraint users_privileges_pk primary key clustered([id]) on [PRIMARY],\n" . " constraint user_privilege_fk foreign key ([id]) references [users] ([id]) on update no action on delete no action\n" - . ")\n" - . "\n" - . "if not exists (select * from sysobjects where name='users_tasks' and xtype='U')\n" + . ")\n", $schema->getTable('users_privileges')->toSQL()); + + $this->assertEquals("if not exists (select * from sysobjects where name='users_tasks' and xtype='U')\n" . "create table [users_tasks] (\n" . " [task_id] [int] identity(1,1) not null,\n" . " [user_id] [int] not null,\n" @@ -429,16 +433,15 @@ public function testCreateTables() { . " [details] [varchar](1500) not null,\n" . " constraint users_tasks_pk primary key clustered([task_id]) on [PRIMARY],\n" . " constraint user_task_fk foreign key ([user_id]) references [users] ([id]) on update no action on delete no action\n" - . ")\n" - . "\n" - . "if not exists (select * from sysobjects where name='profile_pics' and xtype='U')\n" + . ")\n", $schema->getTable('users_tasks')->toSQL()); + + $this->assertEquals("if not exists (select * from sysobjects where name='profile_pics' and xtype='U')\n" . "create table [profile_pics] (\n" . " [user_id] [int] not null,\n" . " [pic] [binary](1) not null,\n" . " constraint profile_pics_pk primary key clustered([user_id]) on [PRIMARY],\n" . " constraint user_profile_pic_fk foreign key ([user_id]) references [users] ([id]) on update no action on delete no action\n" - . ")" - , $schema->getLastQuery()); + . ")\n", $schema->getTable('profile_pics')->toSQL()); } /** * @@ -553,7 +556,7 @@ public function testDelete01() { */ public function testInsert03() { $schema = new MSSQLTestSchema(); - $schema->createTables()->execute(); + $schema->createTables(); // Clear table first to ensure clean state $schema->table('users')->delete()->execute(); // Reset identity to start from 1 diff --git a/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php b/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php index 8ea24b0..c495050 100644 --- a/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php +++ b/tests/WebFiori/Tests/Database/MySql/MySQLQueryBuilderTest.php @@ -40,7 +40,11 @@ public function testCreateTables() { "set character_set_client='utf8'", "set character_set_results='utf8'" ],$schema->getExecutedQueries()); - $schema->createTables(); + + // Verify each table's SQL individually since createTables() now executes directly + $tables = $schema->getTables(); + $this->assertCount(4, $tables); + $this->assertEquals("create table if not exists `users` (\n" . " `id` int not null unique auto_increment,\n" . " `first_name` varchar(15) not null collate utf8mb4_unicode_520_ci,\n" @@ -50,8 +54,9 @@ public function testCreateTables() { . ")\n" . "engine = InnoDB\n" . "default charset = utf8mb4\n" - . "collate = utf8mb4_unicode_520_ci;\n" - . "create table if not exists `users_privileges` (\n" + . "collate = utf8mb4_unicode_520_ci;", $schema->getTable('users')->toSQL()); + + $this->assertEquals("create table if not exists `users_privileges` (\n" . " `id` int not null unique,\n" . " `can_edit_price` bit(1) not null default b'0',\n" . " `can_change_username` bit(1) not null,\n" @@ -61,8 +66,9 @@ public function testCreateTables() { . ")\n" . "engine = InnoDB\n" . "default charset = utf8mb4\n" - . "collate = utf8mb4_unicode_520_ci;\n" - . "create table if not exists `users_tasks` (\n" + . "collate = utf8mb4_unicode_520_ci;", $schema->getTable('users_privileges')->toSQL()); + + $this->assertEquals("create table if not exists `users_tasks` (\n" . " `task_id` int not null unique auto_increment,\n" . " `user_id` int not null comment 'The ID of the user who must perform the activity.',\n" . " `created_on` timestamp not null default now(),\n" @@ -75,8 +81,9 @@ public function testCreateTables() { . "comment 'The tasks at which each user can have.'\n" . "engine = InnoDB\n" . "default charset = utf8mb4\n" - . "collate = utf8mb4_unicode_520_ci;\n" - . "create table if not exists `profile_pics` (\n" + . "collate = utf8mb4_unicode_520_ci;", $schema->getTable('users_tasks')->toSQL()); + + $this->assertEquals("create table if not exists `profile_pics` (\n" . " `user_id` int not null unique,\n" . " `pic` mediumblob not null,\n" . " constraint `profile_pics_pk` primary key (`user_id`),\n" @@ -84,7 +91,7 @@ public function testCreateTables() { . ")\n" . "engine = InnoDB\n" . "default charset = utf8mb4\n" - . "collate = utf8mb4_unicode_520_ci;", $schema->getLastQuery()); + . "collate = utf8mb4_unicode_520_ci;", $schema->getTable('profile_pics')->toSQL()); } /** * diff --git a/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php b/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php index e9eeabe..9c38496 100644 --- a/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php +++ b/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php @@ -12,8 +12,8 @@ class TestEntity { public ?int $id = null; - public string $name; - public int $value; + public string $name = ''; + public int $value = 0; public function __construct(?int $id = null, string $name = '', int $value = 0) { $this->id = $id; @@ -76,8 +76,7 @@ public static function setUpBeforeClass(): void { } public static function tearDownAfterClass(): void { - self::$db->setQuery('DROP TABLE IF EXISTS test_entities'); - self::$db->execute(); + self::$db->raw('DROP TABLE IF EXISTS test_entities')->execute(); } protected function setUp(): void { diff --git a/tests/WebFiori/Tests/Database/Repository/EagerLoadingTest.php b/tests/WebFiori/Tests/Database/Repository/EagerLoadingTest.php new file mode 100644 index 0000000..5f86572 --- /dev/null +++ b/tests/WebFiori/Tests/Database/Repository/EagerLoadingTest.php @@ -0,0 +1,354 @@ +id = (int) $row['id']; + $author->name = $row['name']; + return $author; + } + + protected function toArray(object $entity): array { + return [ + 'id' => $entity->id, + 'name' => $entity->name + ]; + } +} + +class PostRepository extends AbstractRepository { + protected function getTableClass(): string { + return PostsTable::class; + } + + protected function getTableName(): string { + return 'posts'; + } + + protected function getIdField(): string { + return 'id'; + } + + protected function toEntity(array $row): object { + $post = new Post(); + $post->id = (int) $row['id']; + $post->title = $row['title']; + $post->authorId = (int) ($row['author-id'] ?? $row['author_id'] ?? 0); + return $post; + } + + protected function toArray(object $entity): array { + return [ + 'id' => $entity->id, + 'title' => $entity->title, + 'author-id' => $entity->authorId + ]; + } +} + +class EagerLoadingTest extends TestCase { + private static ?Database $db = null; + private static ?AuthorRepository $authorRepo = null; + private static ?PostRepository $postRepo = null; + + public static function setUpBeforeClass(): void { + $conn = new ConnectionInfo('mysql', 'root', '123456', 'testing_db', '127.0.0.1'); + self::$db = new Database($conn); + + self::$db->raw('DROP TABLE IF EXISTS posts')->execute(); + self::$db->raw('DROP TABLE IF EXISTS authors')->execute(); + + // Use attribute-based tables for consistency + self::$db->addTable(AttributeTableBuilder::build(AuthorsTable::class, 'mysql')); + self::$db->addTable(AttributeTableBuilder::build(PostsTable::class, 'mysql')); + self::$db->createTables(); + + self::$authorRepo = new AuthorRepository(self::$db); + self::$postRepo = new PostRepository(self::$db); + } + + public static function tearDownAfterClass(): void { + self::$db->raw('DROP TABLE IF EXISTS posts')->execute(); + self::$db->raw('DROP TABLE IF EXISTS authors')->execute(); + } + + protected function setUp(): void { + self::$db->table('posts')->delete()->execute(); + self::$db->table('authors')->delete()->execute(); + } + + private function seedData(): array { + // Create authors + self::$db->table('authors')->insert(['name' => 'Ahmad'])->execute(); + self::$db->table('authors')->insert(['name' => 'Fatima'])->execute(); + + // Get actual IDs + $authors = self::$db->table('authors')->select()->execute()->fetchAll(); + $ahmadId = null; + $fatimaId = null; + foreach ($authors as $a) { + if ($a['name'] === 'Ahmad') $ahmadId = $a['id']; + if ($a['name'] === 'Fatima') $fatimaId = $a['id']; + } + + // Create posts with actual IDs + self::$db->table('posts')->insert(['title' => 'Post 1 by Ahmad', 'author-id' => $ahmadId])->execute(); + self::$db->table('posts')->insert(['title' => 'Post 2 by Ahmad', 'author-id' => $ahmadId])->execute(); + self::$db->table('posts')->insert(['title' => 'Post 3 by Fatima', 'author-id' => $fatimaId])->execute(); + + return ['ahmad' => $ahmadId, 'fatima' => $fatimaId]; + } + + public function testFindAllWithoutEagerLoading() { + $this->seedData(); + + $authors = self::$authorRepo->findAll(); + + $this->assertCount(2, $authors); + $this->assertEmpty($authors[0]->posts); + $this->assertEmpty($authors[1]->posts); + } + + public function testFindAllWithHasManyEagerLoading() { + $this->seedData(); + + $authors = self::$authorRepo->with(['posts'])->findAll(); + + $this->assertCount(2, $authors); + + // Ahmad has 2 posts + $ahmad = $authors[0]->name === 'Ahmad' ? $authors[0] : $authors[1]; + $this->assertCount(2, $ahmad->posts); + $this->assertInstanceOf(Post::class, $ahmad->posts[0]); + + // Fatima has 1 post + $fatima = $authors[0]->name === 'Fatima' ? $authors[0] : $authors[1]; + $this->assertCount(1, $fatima->posts); + } + + public function testFindByIdWithHasManyEagerLoading() { + $ids = $this->seedData(); + + $author = self::$authorRepo->with(['posts'])->findById($ids['ahmad']); + + $this->assertNotNull($author); + $this->assertEquals('Ahmad', $author->name); + $this->assertCount(2, $author->posts); + } + + public function testFindAllWithBelongsToEagerLoading() { + $this->seedData(); + + $posts = self::$postRepo->with(['author'])->findAll(); + + $this->assertCount(3, $posts); + + foreach ($posts as $post) { + $this->assertNotNull($post->author); + $this->assertInstanceOf(Author::class, $post->author); + } + + // Verify correct author assignment + $post1 = array_filter($posts, fn($p) => $p->title === 'Post 1 by Ahmad'); + $post1 = reset($post1); + $this->assertEquals('Ahmad', $post1->author->name); + } + + public function testFindByIdWithBelongsToEagerLoading() { + $this->seedData(); + + // Get first post ID + $posts = self::$db->table('posts')->select()->execute()->fetchAll(); + $postId = $posts[0]['id']; + + $post = self::$postRepo->with(['author'])->findById($postId); + + $this->assertNotNull($post); + $this->assertNotNull($post->author); + } + + public function testPaginateWithEagerLoading() { + $this->seedData(); + + $page = self::$authorRepo->with(['posts'])->paginate(1, 10); + + $this->assertEquals(2, $page->getTotalItems()); + $items = $page->getItems(); + + $hasPostsCount = 0; + foreach ($items as $author) { + if (!empty($author->posts)) { + $hasPostsCount++; + } + } + $this->assertEquals(2, $hasPostsCount); + } + + public function testWithReturnsClone() { + $repo1 = self::$authorRepo; + $repo2 = $repo1->with(['posts']); + + $this->assertNotSame($repo1, $repo2); + } + + public function testUnknownRelationThrowsException() { + $this->seedData(); + + $this->expectException(\WebFiori\Database\Repository\RepositoryException::class); + $this->expectExceptionMessage('Unknown relationship: unknown'); + + self::$authorRepo->with(['unknown'])->findAll(); + } + + public function testEmptyResultWithEagerLoading() { + // No data seeded + $authors = self::$authorRepo->with(['posts'])->findAll(); + + $this->assertEmpty($authors); + } + + public function testAuthorWithNoPosts() { + // Create author without posts + $author = new Author(); + $author->name = 'No Posts Author'; + self::$authorRepo->save($author); + + $authors = self::$authorRepo->with(['posts'])->findAll(); + + $this->assertCount(1, $authors); + $this->assertEmpty($authors[0]->posts); + } + + public function testWithJoinFindAll() { + $this->seedData(); + + $posts = self::$postRepo->withJoin(['author'])->findAll(); + + $this->assertCount(3, $posts); + foreach ($posts as $post) { + $this->assertNotNull($post->author); + $this->assertInstanceOf(Author::class, $post->author); + } + } + + public function testWithJoinFindById() { + $this->seedData(); + + $posts = self::$db->table('posts')->select()->execute()->fetchAll(); + $postId = $posts[0]['id']; + + $post = self::$postRepo->withJoin(['author'])->findById($postId); + + $this->assertNotNull($post); + $this->assertNotNull($post->author); + $this->assertInstanceOf(Author::class, $post->author); + } + + public function testWithJoinThrowsExceptionForHasMany() { + $this->expectException(\WebFiori\Database\Repository\RepositoryException::class); + $this->expectExceptionMessage("Cannot use withJoin() for hasMany relation 'posts'"); + + self::$authorRepo->withJoin(['posts'])->findAll(); + } + + public function testWithJoinThrowsExceptionForUnknownRelation() { + $this->expectException(\WebFiori\Database\Repository\RepositoryException::class); + $this->expectExceptionMessage('Unknown relationship: unknown'); + + self::$postRepo->withJoin(['unknown'])->findAll(); + } + + public function testWithJoinReturnsClone() { + $repo1 = self::$postRepo; + $repo2 = $repo1->withJoin(['author']); + + $this->assertNotSame($repo1, $repo2); + } + + public function testWithAcceptsString() { + $this->seedData(); + + $posts = self::$postRepo->with('author')->findAll(); + + $this->assertCount(3, $posts); + foreach ($posts as $post) { + $this->assertNotNull($post->author); + } + } + + public function testWithJoinAcceptsString() { + $this->seedData(); + + $posts = self::$db->table('posts')->select()->execute()->fetchAll(); + $postId = $posts[0]['id']; + + $post = self::$postRepo->withJoin('author')->findById($postId); + + $this->assertNotNull($post); + $this->assertNotNull($post->author); + } +}