diff --git a/.github/workflows/php84.yaml b/.github/workflows/php84.yaml
index 53f26da..4b233b8 100644
--- a/.github/workflows/php84.yaml
+++ b/.github/workflows/php84.yaml
@@ -100,29 +100,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: code-coverage
- path: php-8.4-coverage.xml
-
-
- code-coverage:
- name: Coverage
- needs: test
- uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
- with:
- php-version: '8.1'
- coverage-file: 'php-8.1-coverage.xml'
- secrets:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-
- code-quality:
- name: Code Quality
- needs: test
- uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@main
- 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
- with:
- branch: 'main'
\ No newline at end of file
+ path: php-8.4-coverage.xml
\ No newline at end of file
diff --git a/.github/workflows/php85.yaml b/.github/workflows/php85.yaml
new file mode 100644
index 0000000..31ebd38
--- /dev/null
+++ b/.github/workflows/php85.yaml
@@ -0,0 +1,128 @@
+name: PHP 8.5
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main , dev]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ env:
+ SA_SQL_SERVER_PASSWORD: ${{ secrets.SA_SQL_SERVER_PASSWORD }}
+ MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
+
+ services:
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server:2019-latest
+ env:
+ SA_PASSWORD: ${{ secrets.SA_SQL_SERVER_PASSWORD }}
+ ACCEPT_EULA: Y
+ MSSQL_PID: Express
+ ports:
+ - "1433:1433"
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
+ MYSQL_DATABASE: testing_db
+ MYSQL_ROOT_HOST: '%'
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+ strategy:
+ fail-fast: true
+
+ name: Run PHPUnit Tests
+
+ steps:
+ - name: Clone Repo
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.5
+ extensions: mysqli, mbstring, sqlsrv
+ tools: phpunit:11.5.27, composer
+
+ - name: Install ODBC Driver for SQL Server
+ run: |
+ curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
+ curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
+ sudo apt update
+ sudo ACCEPT_EULA=Y apt install mssql-tools18 unixodbc-dev msodbcsql18
+
+ - name: Wait for SQL Server
+ run: |
+ for i in {1..12}; do
+ if /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P '${{ secrets.SA_SQL_SERVER_PASSWORD }}' -Q 'SELECT 1' -C > /dev/null 2>&1; then
+ echo "SQL Server is ready"
+ break
+ fi
+ echo "Waiting for SQL Server... ($i/12)"
+ sleep 10
+ done
+
+ - name: Create SQL Server Database
+ run: /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P '${{ secrets.SA_SQL_SERVER_PASSWORD }}' -Q 'create database testing_db' -C
+
+ - name: Setup MySQL Client
+ run: |
+ sudo apt update
+ sudo apt install mysql-client-core-8.0
+
+ - name: Wait for MySQL
+ run: |
+ until mysqladmin ping -h 127.0.0.1 --silent; do
+ echo 'waiting for mysql...'
+ sleep 1
+ done
+
+ - name: Create MySQL Database
+ run: |
+ mysql -h 127.0.0.1 -u root -p${{ secrets.MYSQL_ROOT_PASSWORD }} -e "CREATE DATABASE IF NOT EXISTS testing_db;"
+
+ - name: Install Dependencies
+ run: composer install --prefer-source --no-interaction
+
+ - name: Execute Tests
+ run: phpunit --configuration=tests/phpunit10.xml --coverage-clover=clover.xml --stop-on-failure
+
+ - name: Rename coverage report
+ run: |
+ mv clover.xml php-8.5-coverage.xml
+
+ - name: Upload Coverage Report
+ uses: actions/upload-artifact@v4
+ with:
+ name: code-coverage
+ path: php-8.5-coverage.xml
+
+
+ code-coverage:
+ name: Coverage
+ needs: test
+ uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
+ with:
+ php-version: '8.5'
+ coverage-file: 'php-8.5-coverage.xml'
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ code-quality:
+ name: Code Quality
+ needs: test
+ uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@main
+ 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
+ with:
+ branch: 'main'
\ No newline at end of file
diff --git a/README.md b/README.md
index f42c8e2..a3b670f 100644
--- a/README.md
+++ b/README.md
@@ -1,446 +1,398 @@
-# Webfiori Database Abstraction Layer
-
-Database abstraction layer of WebFiori framework.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## Content
-
-* [Supported PHP Versions](#supported-php-versions)
-* [Supported Databases](#supported-databases)
-* [Features](#features)
-* [Installation](#installation)
-* [Usage](#usage)
- * [Connecting to Database](#connecting-to-database)
- * [Running Basic SQL Queries](#running-basic-sql-queries)
- * [Insert Query](#insert-query)
- * [Select Query](#select-query)
- * [Update Query](#update-query)
- * [Delete Query](#delete-query)
- * [Building Database Structure](#building-database-structure)
- * [Creating Table Blueprint](#creating-table-blueprint)
- * [Seeding Structure to Database](#seeding-structure-to-database)
- * [Creating Entity Classes and Using Them](#creating-entity-classes-and-using-them)
- * [Creating an Entity Class](#creating-an-entity-class)
- * [Using Entity Class](#using-entity-class)
- * [Database Migrations](#database-migrations)
- * [Database Seeders](#database-seeders)
- * [Performance Monitoring](#performance-monitoring)
- * [Transactions](#transactions)
-
-## Supported PHP Versions
-| Build Status |
-|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
-|
|
-|
|
-|
|
-|
|
-
-## Supported Databases
-- MySQL
-- MSSQL
-
-## Features
-* Building your database structure within PHP
-* Fast and easy to use query builder
-* Database abstraction which makes it easy to migrate your system to different DBMS
-* Database migrations and seeders
-* Performance monitoring and query analysis
-* Entity mapping for object-relational mapping
-* Transaction support with automatic rollback
-
-## Installation
-To install the library using composer, add following dependency to `composer.json`: `"webfiori/database":"*"`
-
-## Usage
-
-### Connecting to Database
-
-Connecting to a database is simple. First step is to define database connection information using the class `ConnectionInfo`. Later, the instance can be used to establish a connection to the database using the class `Database`.
-
-```php
-use WebFiori\Database\ConnectionInfo;
-use WebFiori\Database\Database;
-
-// This assumes that MySQL is installed on localhost
-// and root password is set to '123456'
-// and there is a schema with name 'testing_db'
-$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
-$database = new Database($connection);
-```
-
-### Running Basic SQL Queries
-
-Most common SQL queries that will be executed in any relational DBMS are insert, select, update, and delete. Following examples shows how the 4 types can be constructed.
-
-For every query, the table that the query will be executed on must be specified. To specify the table, use the method `Database::table(string $tblName)`. The method will return an instance of the class `AbstractQuery`. The class `AbstractQuery` has many methods which are used to further build the query. Commonly used methods include the following:
-
-* `AbstractQuery::insert(array $cols)`: Construct an insert query.
-* `AbstractQuery::select(array $cols)`: Construct a select query.
-* `AbstractQuery::update(array $cols)`: Construct an update query.
-* `AbstractQuery::delete()`: Construct a delete query.
-* `AbstractQuery::where($col, $val)`: Adds a condition to the query.
-
-After building the query, the method `AbstractQuery::execute()` can be called to execute the query. If the query is a `select` query, the method will return an instance of the class `ResultSet`. The instance can be used to traverse the records that was returned by the DBMS.
-
-#### Insert Query
-
-Insert query is used to add records to the database. To execute an insert query, use the method `AbstractQuery::insert(array $cols)`. The method accepts one parameter. The parameter is an associative array. The indices of the array are columns names and the values of the indices are the values that will be inserted.
-
-```php
-$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
-$database = new Database($connection);
-
-$database->table('posts')->insert([
- 'title' => 'Super New Post',
- 'author' => 'Me'
-])->execute();
-```
-
-#### Select Query
-
-A select query is used to fetch database records and use them in application logic. To execute a select query, use the method `AbstractQuery::select(array $cols)`. The method accepts one optional parameter. The parameter is an array that holds the names of the columns that will be selected. In this case, the method `AbstractQuery::execute()` will return an object of type `ResultSet`. The result set will contain raw fetched records as big array that holds the actual records. Each record is stored as an associative array.
-
-```php
-$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
-$database = new Database($connection);
-
-// This assumes that we have a table called 'posts' in the database.
-$resultSet = $database->table('posts')->select()->execute();
-
-foreach ($resultSet as $record) {
- echo $record['title'];
-}
-```
-
-It is possible to add a condition to the select query using the method `AbstractQuery::where()`.
-
-```php
-$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
-$database = new Database($connection);
-
-// This assumes that we have a table called 'posts' in the database.
-$resultSet = $database->table('posts')
- ->select()
- ->where('author', 'Ibrahim')
- ->execute();
-
-foreach ($resultSet as $record) {
- echo $record['title'];
-}
-```
-
-#### Update Query
-
-Update query is used to update a single record or multiple records. To execute an update query, use the method `AbstractQuery::update(array $cols)`. The method accepts one parameter. The parameter is an associative array. The indices of the array are columns names and the values of the indices are the updated values. Usually, for any update query, a `where` condition will be included. To include a `where` condition, the method `AbstractQuery::where()` can be used.
-
-```php
-$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
-$database = new Database($connection);
-
-$database->table('posts')->update([
- 'title' => 'Super New Post By Ibrahim',
-])->where('author', 'Ibrahim')
- ->andWhere('created-on', '2023-03-24')
- ->execute();
-```
-
-#### Delete Query
-
-This query is used to delete specific record from the database. To execute delete query, use the method `AbstractQuery::delete()`. A `where` condition should be included to delete specific record. To include a `where` condition, the method `AbstractQuery::where()` can be used.
-
-```php
-$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
-$database = new Database($connection);
-
-$database->table('posts')->delete()->where('author', 'Ibrahim')->execute();
-```
-
-### Building Database Structure
-
-One of the features of the library is the ability to define database structure in the source code and later, seed the created structure to create database tables. The blueprint of tables are represented by the class `Table`. The main aim of the blueprint is to make sure that data types in database are represented correctly in the source code.
-
-#### Creating Table Blueprint
-
-Each blueprint must have following attributes defined:
-
-* Name of the blueprint (database table name).
-* Columns and their properties such as data type.
-* Any relations with other tables.
-
-The method `Database::createBlueprint()` is used to create a table based on connected DBMS. The method will return an instance of the class `Table` which can be used to further customize the blueprint.
-
-```php
-use WebFiori\Database\ColOption;
-use WebFiori\Database\DataType;
-
-$database->createBlueprint('users_information')->addColumns([
- 'id' => [
- ColOption::TYPE => DataType::INT,
- ColOption::SIZE => 5,
- ColOption::PRIMARY => true,
- ColOption::AUTO_INCREMENT => true
- ],
- 'first-name' => [
- ColOption::TYPE => DataType::VARCHAR,
- ColOption::SIZE => 15
- ],
- 'last-name' => [
- ColOption::TYPE => DataType::VARCHAR,
- ColOption::SIZE => 15
- ],
- 'email' => [
- ColOption::TYPE => DataType::VARCHAR,
- ColOption::SIZE => 128
- ]
-]);
-```
-
-#### Seeding Structure to Database
-
-After creating all blueprints, a query must be structured and executed to create database tables. Building the query can be performed using the method `Database::createTables()`. After calling this method, the method `Database::execute()` must be called to create all database tables.
-
-```php
-// Build the query
-$database->createTables();
-
-// Execute
-$database->execute();
-```
-
-### Creating Entity Classes and Using Them
-
-Entity classes are classes which are based on blueprints (or tables). They can be used to map records of tables to objects. Every blueprint will have an instance of the class `EntityMapper` which can be used to create an entity class.
-
-Entity classes that are generated using the class `EntityMapper` are special. They will have one static method with name `map()` which can automatically map a record to an instance of the entity.
-
-#### Creating an Entity Class
-
-First step in creating an entity is to have the blueprint at which the entity will be based on. From the blueprint, an instance of the class `EntityMapper` is generated. After having the instance, the properties of the entity is set such as its name, namespace and where it will be created. Finally, the method `EntityMapper::create()` can be invoked to write the source code of the class.
-
-```php
-$blueprint = $database->getTable('users_information');
-
-// Get entity mapper
-$entityMapper = $blueprint->getEntityMapper();
-
-// Set properties of the entity
-$entityMapper->setEntityName('UserInformation');
-$entityMapper->setNamespace('');
-$entityMapper->setPath(__DIR__);
-
-// Create the entity. The output will be the class 'UserInformation'.
-$entityMapper->create();
-```
-
-#### Using Entity Class
-
-Entity class can be used to map a record to an object. Each entity will have a special method called `map()`. The method accepts a single parameter which is an associative array that represents fetched record.
-
-The result set instance has one of array methods which is called `map($callback)` This method acts exactly as the function `array_map($callback, $array)`. The return value of the method is another result set with mapped records.
-
-```php
-$resultSet = $database->table('users_information')
- ->select()
- ->execute();
-
-$mappedSet = $resultSet->map(function (array $record) {
- return UserInformation::map($record);
-});
-
-foreach ($mappedSet as $record) {
- // $record is an object of type UserInformation
- echo $record->getFirstName() . ' ' . $record->getLastName() . "\n";
-}
-```
-
-### Database Migrations
-
-Migrations allow you to version control your database schema changes. Each migration represents a specific change to your database structure.
-
-```php
-use WebFiori\Database\Schema\AbstractMigration;
-use WebFiori\Database\Database;
-
-class CreateUsersTable extends AbstractMigration {
-
- public function up(Database $db): void {
- $db->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
- ]
- ]);
-
- $db->createTables();
- $db->execute();
- }
-
- public function down(Database $db): void {
- $db->setQuery("DROP TABLE users");
- $db->execute();
- }
-}
-```
-
-To run migrations, use the SchemaRunner:
-
-```php
-use WebFiori\Database\Schema\SchemaRunner;
-
-$runner = new SchemaRunner($connectionInfo);
-
-// Register migration classes
-$runner->register('CreateUsersTable');
-$runner->register('AddEmailIndex');
-
-// Create schema tracking table
-$runner->createSchemaTable();
-
-// Apply all pending migrations
-$appliedMigrations = $runner->apply();
-
-// Rollback migrations
-$rolledBackMigrations = $runner->rollbackUpTo(null);
-```
-
-### Database Seeders
-
-Seeders allow you to populate your database with sample or default data.
-
-```php
-use WebFiori\Database\Schema\AbstractSeeder;
-use WebFiori\Database\Database;
-
-class UsersSeeder extends AbstractSeeder {
-
- public function run(Database $db): void {
- $db->table('users')->insert([
- 'name' => 'Administrator',
- 'email' => 'admin@example.com'
- ])->execute();
-
- $db->table('users')->insert([
- 'name' => 'John Doe',
- 'email' => 'john@example.com'
- ])->execute();
- }
-}
-```
-
-To run seeders, use the same SchemaRunner:
-
-```php
-use WebFiori\Database\Schema\SchemaRunner;
-
-$runner = new SchemaRunner($connectionInfo);
-
-// Register seeder classes
-$runner->register('UsersSeeder');
-$runner->register('CategoriesSeeder');
-
-// Create schema tracking table
-$runner->createSchemaTable();
-
-// Apply all pending seeders
-$appliedSeeders = $runner->apply();
-```
-
-### Performance Monitoring
-
-The library includes built-in performance monitoring to help you identify slow queries and optimize database performance.
-
-```php
-use WebFiori\Database\Performance\PerformanceOption;
-use WebFiori\Database\Performance\PerformanceAnalyzer;
-
-// Configure performance monitoring
-$database->setPerformanceConfig([
- PerformanceOption::ENABLED => true,
- PerformanceOption::SLOW_QUERY_THRESHOLD => 50, // 50ms threshold
- PerformanceOption::SAMPLING_RATE => 1.0 // Monitor all queries
-]);
-
-// Execute some queries
-$database->table('users')->select()->execute();
-$database->table('posts')->select()->where('status', 'published')->execute();
-
-// Analyze performance
-$analyzer = $database->getPerformanceMonitor()->getAnalyzer();
-
-echo "Total queries: " . $analyzer->getQueryCount() . "\n";
-echo "Average execution time: " . $analyzer->getAverageTime() . "ms\n";
-echo "Performance score: " . $analyzer->getScore() . "\n";
-echo "Slow queries: " . $analyzer->getSlowQueryCount() . "\n";
-
-// Check if performance needs improvement
-if ($analyzer->getScore() === PerformanceAnalyzer::SCORE_NEEDS_IMPROVEMENT) {
- $slowQueries = $analyzer->getSlowQueries();
- foreach ($slowQueries as $metric) {
- echo "Slow query: " . $metric->getQuery() . " (" . $metric->getExecutionTimeMs() . "ms)\n";
- }
-}
-```
-
-### Transactions
-
-Database transactions ensure that multiple operations are executed as a single unit of work. If any operation fails, all operations are rolled back.
-
-```php
-$database->transaction(function (Database $db) {
- // Insert user
- $db->table('users')->insert([
- 'name' => 'John Doe',
- 'email' => 'john@example.com'
- ])->execute();
-
- // Insert user profile
- $db->table('user_profiles')->insert([
- 'user_id' => $db->getLastInsertId(),
- 'bio' => 'Software Developer'
- ])->execute();
-
- // If any query fails, the entire transaction is rolled back
-});
-```
-
-You can also pass additional parameters to the transaction closure:
-
-```php
-$userData = ['name' => 'Jane Doe', 'email' => 'jane@example.com'];
-
-$database->transaction(function (Database $db, array $user) {
- $db->table('users')->insert($user)->execute();
-
- $db->table('user_profiles')->insert([
- 'user_id' => $db->getLastInsertId(),
- 'created_at' => date('Y-m-d H:i:s')
- ])->execute();
-
-}, [$userData]);
-```
+# Webfiori Database Abstraction Layer
+
+Database abstraction layer of WebFiori framework.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Content
+
+* [Supported PHP Versions](#supported-php-versions)
+* [Supported Databases](#supported-databases)
+* [Features](#features)
+* [Installation](#installation)
+* [Usage](#usage)
+ * [Connecting to Database](#connecting-to-database)
+ * [Running Basic SQL Queries](#running-basic-sql-queries)
+ * [Building Database Structure](#building-database-structure)
+ * [Repository Pattern](#repository-pattern)
+ * [Active Record Pattern](#active-record-pattern)
+ * [Entity Generation](#entity-generation)
+ * [Database Migrations](#database-migrations)
+ * [Database Seeders](#database-seeders)
+ * [Performance Monitoring](#performance-monitoring)
+ * [Transactions](#transactions)
+
+## Supported PHP Versions
+| Build Status |
+|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
+|
|
+|
|
+|
|
+|
|
+|
|
+
+## Supported Databases
+- MySQL
+- MSSQL
+
+## Features
+* Building your database structure within PHP
+* Fast and easy to use query builder
+* Database abstraction which makes it easy to migrate your system to different DBMS
+* Repository pattern with `AbstractRepository` for clean data access
+* Active Record pattern support for rapid development
+* PHP 8 attributes for table definitions
+* Database migrations and seeders
+* Performance monitoring and query analysis
+* Entity generation for object-relational mapping
+* Transaction support with automatic rollback
+
+## Installation
+To install the library using composer, add following dependency to `composer.json`: `"webfiori/database":"*"`
+
+## Usage
+
+### Connecting to Database
+
+Connecting to a database is simple. First step is to define database connection information using the class `ConnectionInfo`. Later, the instance can be used to establish a connection to the database using the class `Database`.
+
+```php
+use WebFiori\Database\ConnectionInfo;
+use WebFiori\Database\Database;
+
+$connection = new ConnectionInfo('mysql', 'root', '123456', 'testing_db');
+$database = new Database($connection);
+```
+
+### Running Basic SQL Queries
+
+For every query, the table must be specified using `Database::table(string $tblName)`. The method returns an `AbstractQuery` instance with methods for building queries:
+
+* `insert(array $cols)`: Construct an insert query.
+* `select(array $cols)`: Construct a select query.
+* `update(array $cols)`: Construct an update query.
+* `delete()`: Construct a delete query.
+* `where($col, $val)`: Adds a condition to the query.
+
+After building the query, call `execute()` to run it.
+
+```php
+// Insert
+$database->table('posts')->insert([
+ 'title' => 'Super New Post',
+ 'author' => 'Me'
+])->execute();
+
+// Select
+$resultSet = $database->table('posts')
+ ->select()
+ ->where('author', 'Ibrahim')
+ ->execute();
+
+foreach ($resultSet as $record) {
+ echo $record['title'];
+}
+
+// Update
+$database->table('posts')->update([
+ 'title' => 'Updated Title',
+])->where('id', 1)->execute();
+
+// Delete
+$database->table('posts')->delete()->where('id', 1)->execute();
+```
+
+### Building Database Structure
+
+Define database structure in PHP code using blueprints:
+
+```php
+use WebFiori\Database\ColOption;
+use WebFiori\Database\DataType;
+
+$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
+ ]
+]);
+
+// Create the table
+$database->table('users')->createTable()->execute();
+```
+
+### Repository Pattern
+
+The `AbstractRepository` class provides a clean way to handle data access with separation between entities and database logic.
+
+#### Creating an Entity
+
+```php
+class Product {
+ public ?int $id = null;
+ public string $name;
+ public float $price;
+}
+```
+
+#### Creating a Repository
+
+```php
+use WebFiori\Database\Repository\AbstractRepository;
+
+class ProductRepository extends AbstractRepository {
+ protected function getTableName(): string {
+ return 'products';
+ }
+
+ protected function getIdField(): string {
+ return 'id';
+ }
+
+ protected function toEntity(array $row): object {
+ $product = new Product();
+ $product->id = (int) $row['id'];
+ $product->name = $row['name'];
+ $product->price = (float) $row['price'];
+ return $product;
+ }
+
+ protected function toArray(object $entity): array {
+ return [
+ 'id' => $entity->id,
+ 'name' => $entity->name,
+ 'price' => $entity->price
+ ];
+ }
+}
+```
+
+#### Using the Repository
+
+```php
+$repo = new ProductRepository($database);
+
+// Create
+$product = new Product();
+$product->name = 'Widget';
+$product->price = 29.99;
+$repo->save($product);
+
+// Read
+$product = $repo->findById(1);
+$allProducts = $repo->findAll();
+
+// Update
+$product->price = 24.99;
+$repo->save($product);
+
+// Delete
+$repo->deleteById(1);
+
+// Pagination
+$page = $repo->paginate(page: 1, perPage: 20);
+```
+
+### Active Record Pattern
+
+For rapid development, you can merge entity and repository into a single model class:
+
+```php
+use WebFiori\Database\Attributes\Column;
+use WebFiori\Database\Attributes\Table;
+use WebFiori\Database\DataType;
+use WebFiori\Database\Repository\AbstractRepository;
+
+#[Table(name: 'articles')]
+class Article extends AbstractRepository {
+ #[Column(type: DataType::INT, primary: true, autoIncrement: true)]
+ public ?int $id = null;
+
+ #[Column(type: DataType::VARCHAR, size: 200)]
+ public string $title = '';
+
+ #[Column(type: DataType::TEXT)]
+ public string $content = '';
+
+ protected function getTableName(): string { return 'articles'; }
+ protected function getIdField(): string { return 'id'; }
+
+ protected function toEntity(array $row): object {
+ $article = new self($this->db);
+ $article->id = (int) $row['id'];
+ $article->title = $row['title'];
+ $article->content = $row['content'];
+ return $article;
+ }
+
+ protected function toArray(object $entity): array {
+ return [
+ 'id' => $entity->id,
+ 'title' => $entity->title,
+ 'content' => $entity->content
+ ];
+ }
+}
+```
+
+Usage:
+
+```php
+// Create and save
+$article = new Article($database);
+$article->title = 'Hello World';
+$article->content = 'My first article';
+$article->save();
+
+// Query
+$all = $article->findAll();
+$one = $article->findById(1);
+
+// Update
+$article->title = 'Updated Title';
+$article->save();
+
+// Delete
+$article->deleteById();
+
+// Reload from database
+$fresh = $article->reload();
+```
+
+### Entity Generation
+
+Generate entity classes from table blueprints:
+
+```php
+$blueprint = $database->getTable('users');
+
+$generator = $blueprint->getEntityGenerator('User', __DIR__, 'App\\Entity');
+$generator->generate();
+```
+
+### Database Migrations
+
+Version control your database schema changes:
+
+```php
+use WebFiori\Database\Schema\AbstractMigration;
+
+class CreateUsersTable extends AbstractMigration {
+ public function up(Database $db): void {
+ $db->createBlueprint('users')->addColumns([
+ 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true],
+ 'name' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 100]
+ ]);
+ $db->table('users')->createTable()->execute();
+ }
+
+ public function down(Database $db): void {
+ $db->raw("DROP TABLE users")->execute();
+ }
+}
+```
+
+Run migrations:
+
+```php
+use WebFiori\Database\Schema\SchemaRunner;
+
+$runner = new SchemaRunner($connectionInfo);
+$runner->discoverFromPath(__DIR__ . '/migrations', 'App\\Migrations');
+$runner->createSchemaTable();
+$runner->apply();
+```
+
+### Database Seeders
+
+Populate your database with sample data:
+
+```php
+use WebFiori\Database\Schema\AbstractSeeder;
+
+class UsersSeeder extends AbstractSeeder {
+ public function run(Database $db): void {
+ $db->table('users')->insert([
+ 'name' => 'Administrator',
+ 'email' => 'admin@example.com'
+ ])->execute();
+ }
+}
+```
+
+### Performance Monitoring
+
+Track and analyze query performance:
+
+```php
+use WebFiori\Database\Performance\PerformanceOption;
+
+$database->setPerformanceConfig([
+ PerformanceOption::ENABLED => true,
+ PerformanceOption::SLOW_QUERY_THRESHOLD => 50
+]);
+
+// Execute queries...
+
+$analyzer = $database->getPerformanceMonitor()->getAnalyzer();
+echo "Total queries: " . $analyzer->getQueryCount();
+echo "Slow queries: " . $analyzer->getSlowQueryCount();
+```
+
+### Transactions
+
+Execute multiple operations as a single unit:
+
+```php
+$database->transaction(function (Database $db) {
+ $db->table('users')->insert(['name' => 'John'])->execute();
+ $db->table('profiles')->insert([
+ 'user_id' => $db->getLastInsertId(),
+ 'bio' => 'Developer'
+ ])->execute();
+});
+```
+
+## Examples
+
+See the [examples](examples/) directory for complete working examples:
+
+- [01-basic-connection](examples/01-basic-connection/) - Database connections
+- [02-basic-queries](examples/02-basic-queries/) - CRUD operations
+- [03-table-blueprints](examples/03-table-blueprints/) - Table structures
+- [04-entity-mapping](examples/04-entity-mapping/) - Entity generation
+- [05-transactions](examples/05-transactions/) - Transaction handling
+- [06-migrations](examples/06-migrations/) - Schema migrations
+- [07-seeders](examples/07-seeders/) - Data seeding
+- [08-performance-monitoring](examples/08-performance-monitoring/) - Query analysis
+- [09-multi-result-queries](examples/09-multi-result-queries/) - Stored procedures
+- [10-attribute-based-tables](examples/10-attribute-based-tables/) - PHP 8 attributes
+- [11-repository-pattern](examples/11-repository-pattern/) - Repository pattern
+- [12-clean-architecture](examples/12-clean-architecture/) - Domain separation
+- [13-pagination](examples/13-pagination/) - Pagination techniques
+- [14-active-record-model](examples/14-active-record-model/) - Active Record pattern
diff --git a/WebFiori/Database/Attributes/AttributeTableBuilder.php b/WebFiori/Database/Attributes/AttributeTableBuilder.php
index 4bf219c..b80b5b1 100644
--- a/WebFiori/Database/Attributes/AttributeTableBuilder.php
+++ b/WebFiori/Database/Attributes/AttributeTableBuilder.php
@@ -4,8 +4,7 @@
use ReflectionClass;
use WebFiori\Database\ColOption;
use WebFiori\Database\DataType;
-use WebFiori\Database\MsSql\MSSQLTable;
-use WebFiori\Database\MySql\MySQLTable;
+use WebFiori\Database\Factory\TableFactory;
use WebFiori\Database\Table as TableClass;
class AttributeTableBuilder {
@@ -13,138 +12,139 @@ public static function build(string $entityClass, string $dbType = 'mysql'): Tab
$reflection = new ReflectionClass($entityClass);
$tableAttr = $reflection->getAttributes(Table::class)[0] ?? null;
-
if (!$tableAttr) {
throw new \RuntimeException("Class $entityClass must have #[Table] attribute");
}
$tableConfig = $tableAttr->newInstance();
-
- $table = $dbType === 'mysql'
- ? new MySQLTable($tableConfig->name)
- : new MSSQLTable($tableConfig->name);
-
- if ($tableConfig->comment) {
- $table->setComment($tableConfig->comment);
- }
-
$columns = [];
$foreignKeys = [];
- // Check for class-level Column attributes
$classColumnAttrs = $reflection->getAttributes(Column::class);
if (!empty($classColumnAttrs)) {
- // Class-level approach: columns defined at class level
foreach ($classColumnAttrs as $columnAttr) {
$columnConfig = $columnAttr->newInstance();
$columnKey = $columnConfig->name ?? throw new \RuntimeException("Column name is required for class-level attributes");
-
- $columns[$columnKey] = [
- ColOption::TYPE => $columnConfig->type,
- ColOption::NAME => $columnConfig->name,
- ColOption::SIZE => $columnConfig->size,
- ColOption::SCALE => $columnConfig->scale,
- ColOption::PRIMARY => $columnConfig->primary,
- ColOption::UNIQUE => $columnConfig->unique,
- ColOption::NULL => $columnConfig->nullable,
- ColOption::AUTO_INCREMENT => $columnConfig->autoIncrement,
- ColOption::IDENTITY => $columnConfig->identity,
- ColOption::AUTO_UPDATE => $columnConfig->autoUpdate,
- ColOption::DEFAULT => $columnConfig->default,
- ColOption::COMMENT => $columnConfig->comment,
- ColOption::VALIDATOR => $columnConfig->callback
- ];
+ $columns[$columnKey] = self::columnConfigToArray($columnConfig);
}
- // Check for class-level ForeignKey attributes
- $classFkAttrs = $reflection->getAttributes(ForeignKey::class);
-
- foreach ($classFkAttrs as $fkAttr) {
- $fkConfig = $fkAttr->newInstance();
- $foreignKeys[] = [
- 'property' => $fkConfig->column,
- 'config' => $fkConfig
- ];
+ foreach ($reflection->getAttributes(ForeignKey::class) as $fkAttr) {
+ $foreignKeys[] = $fkAttr->newInstance();
}
} else {
- // Property-level approach: columns defined on properties
foreach ($reflection->getProperties() as $property) {
$columnAttrs = $property->getAttributes(Column::class);
-
if (empty($columnAttrs)) {
continue;
}
$columnConfig = $columnAttrs[0]->newInstance();
$columnKey = self::propertyToKey($property->getName());
+ $columns[$columnKey] = self::columnConfigToArray($columnConfig);
- $columns[$columnKey] = [
- ColOption::TYPE => $columnConfig->type,
- ColOption::NAME => $columnConfig->name,
- ColOption::SIZE => $columnConfig->size,
- ColOption::SCALE => $columnConfig->scale,
- ColOption::PRIMARY => $columnConfig->primary,
- ColOption::UNIQUE => $columnConfig->unique,
- ColOption::NULL => $columnConfig->nullable,
- ColOption::AUTO_INCREMENT => $columnConfig->autoIncrement,
- ColOption::IDENTITY => $columnConfig->identity,
- ColOption::AUTO_UPDATE => $columnConfig->autoUpdate,
- ColOption::DEFAULT => $columnConfig->default,
- ColOption::COMMENT => $columnConfig->comment,
- ColOption::VALIDATOR => $columnConfig->callback
- ];
-
- $fkAttrs = $property->getAttributes(ForeignKey::class);
-
- foreach ($fkAttrs as $fkAttr) {
- $fkConfig = $fkAttr->newInstance();
- $foreignKeys[] = [
- 'property' => $columnKey,
- 'config' => $fkConfig
- ];
+ foreach ($property->getAttributes(ForeignKey::class) as $fkAttr) {
+ $fk = $fkAttr->newInstance();
+ // For property-level FK, the local column is the property itself
+ $foreignKeys[] = ['localColumn' => $columnKey, 'config' => $fk];
}
}
}
- $table->addColumns($columns);
+ $table = TableFactory::create($dbType, $tableConfig->name, $columns);
- // Store table references for foreign keys
- $tableRegistry = [];
+ if ($tableConfig->comment) {
+ $table->setComment($tableConfig->comment);
+ }
+ // Add foreign keys
foreach ($foreignKeys as $fk) {
- $refTableName = $fk['config']->table;
- $refColName = $fk['config']->column;
-
- // Create a minimal table reference if not exists
- if (!isset($tableRegistry[$refTableName])) {
- $refTable = $dbType === 'mysql'
- ? new MySQLTable($refTableName)
- : new MSSQLTable($refTableName);
-
- // Add the referenced column to make FK work
- $refTable->addColumns([
- $refColName => [
- ColOption::TYPE => DataType::INT,
- ColOption::PRIMARY => true
- ]
- ]);
-
- $tableRegistry[$refTableName] = $refTable;
+ if ($fk instanceof ForeignKey) {
+ // Class-level FK
+ self::addForeignKey($table, $fk, $dbType);
+ } else {
+ // Property-level FK
+ self::addPropertyForeignKey($table, $fk['localColumn'], $fk['config'], $dbType);
}
-
- $table->addReference(
- $tableRegistry[$refTableName],
- [$fk['property'] => $refColName],
- $fk['config']->name ?? 'fk_'.$fk['property'],
- $fk['config']->onUpdate,
- $fk['config']->onDelete
- );
}
return $table;
}
+ private static function addForeignKey(TableClass $table, ForeignKey $fk, string $dbType): void {
+ $refTableName = self::resolveTableName($fk->table);
+ $columnsMap = $fk->getColumnsMap();
+
+ // Build reference columns for the referenced table
+ $refColumns = [];
+ $mapping = [];
+
+ foreach ($columnsMap as $local => $ref) {
+ if (is_int($local)) {
+ // Simple array ['col1', 'col2'] - same name on both sides
+ $local = $ref;
+ }
+ $refColumns[$ref] = [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true];
+ $mapping[$local] = $ref;
+ }
+
+ $refTable = TableFactory::create($dbType, $refTableName, $refColumns);
+
+ $table->addReference(
+ $refTable,
+ $mapping,
+ $fk->name ?? 'fk_'.implode('_', array_keys($mapping)),
+ $fk->onUpdate,
+ $fk->onDelete
+ );
+ }
+
+ private static function addPropertyForeignKey(TableClass $table, string $localColumn, ForeignKey $fk, string $dbType): void {
+ $refTableName = self::resolveTableName($fk->table);
+ $refColName = $fk->column ?? $localColumn;
+
+ $refTable = TableFactory::create($dbType, $refTableName, [
+ $refColName => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true]
+ ]);
+
+ $table->addReference(
+ $refTable,
+ [$localColumn => $refColName],
+ $fk->name ?? 'fk_'.$localColumn,
+ $fk->onUpdate,
+ $fk->onDelete
+ );
+ }
+
+ private static function resolveTableName(string $tableOrClass): string {
+ if (class_exists($tableOrClass)) {
+ $reflection = new ReflectionClass($tableOrClass);
+ $tableAttr = $reflection->getAttributes(Table::class)[0] ?? null;
+ if ($tableAttr) {
+ return $tableAttr->newInstance()->name;
+ }
+ }
+ return $tableOrClass;
+ }
+
+ private static function columnConfigToArray(Column $config): array {
+ return [
+ ColOption::TYPE => $config->type,
+ ColOption::NAME => $config->name,
+ ColOption::SIZE => $config->size,
+ ColOption::SCALE => $config->scale,
+ ColOption::PRIMARY => $config->primary,
+ ColOption::UNIQUE => $config->unique,
+ ColOption::NULL => $config->nullable,
+ ColOption::AUTO_INCREMENT => $config->autoIncrement,
+ ColOption::IDENTITY => $config->identity,
+ ColOption::AUTO_UPDATE => $config->autoUpdate,
+ ColOption::DEFAULT => $config->default,
+ ColOption::COMMENT => $config->comment,
+ ColOption::VALIDATOR => $config->callback
+ ];
+ }
+
private static function propertyToKey(string $propertyName): string {
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $propertyName));
}
diff --git a/WebFiori/Database/Attributes/ForeignKey.php b/WebFiori/Database/Attributes/ForeignKey.php
index e2b37a3..aaafa40 100644
--- a/WebFiori/Database/Attributes/ForeignKey.php
+++ b/WebFiori/Database/Attributes/ForeignKey.php
@@ -2,15 +2,32 @@
namespace WebFiori\Database\Attributes;
use Attribute;
+use InvalidArgumentException;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ForeignKey {
public function __construct(
public string $table,
- public string $column,
+ public ?string $column = null,
+ public array $columns = [],
public ?string $name = null,
public string $onUpdate = 'set null',
public string $onDelete = 'set null'
) {
+ if ($column !== null && !empty($columns)) {
+ throw new InvalidArgumentException(
+ "ForeignKey: Use either 'column' or 'columns', not both"
+ );
+ }
+ }
+
+ /**
+ * Get columns mapping as array ['localCol' => 'refCol']
+ */
+ public function getColumnsMap(): array {
+ if ($this->column !== null) {
+ return [$this->column];
+ }
+ return $this->columns;
}
}
diff --git a/WebFiori/Database/Factory/TableFactory.php b/WebFiori/Database/Factory/TableFactory.php
index d2027ed..b120868 100644
--- a/WebFiori/Database/Factory/TableFactory.php
+++ b/WebFiori/Database/Factory/TableFactory.php
@@ -24,7 +24,7 @@
class TableFactory {
public static function create(string $database, string $name, array $cols = []) : Table {
if (!in_array($database, ConnectionInfo::SUPPORTED_DATABASES)) {
- throw new DatabaseException('Not support database: '.$database);
+ throw new DatabaseException('Not support database: '.$database.'. Supported: '.implode(', ', ConnectionInfo::SUPPORTED_DATABASES));
}
if ($database == 'mssql') {
diff --git a/WebFiori/Database/Repository/AbstractRepository.php b/WebFiori/Database/Repository/AbstractRepository.php
index 2dd8c64..eb9cbfc 100644
--- a/WebFiori/Database/Repository/AbstractRepository.php
+++ b/WebFiori/Database/Repository/AbstractRepository.php
@@ -1,22 +1,37 @@
db = $db;
}
+ /**
+ * Returns the total number of records in the table.
+ *
+ * @return int Total record count.
+ */
public function count(): int {
$result = $this->db->table($this->getTableName())
->selectCount(null, 'total')
@@ -25,20 +40,40 @@ public function count(): int {
return (int) $result->fetch()['total'];
}
+ /**
+ * Deletes all records from the table.
+ */
public function deleteAll(): void {
$this->db->table($this->getTableName())
->delete()
->execute();
}
- public function deleteById(mixed $id): void {
+ /**
+ * Deletes a record by its ID.
+ *
+ * If no ID is passed, uses the ID from $this.
+ *
+ * @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.
+ */
+ public function deleteById(mixed $id = null): void {
+ $id = $id ?? $this->getEntityId();
+ if ($id === null) {
+ throw new \InvalidArgumentException('Cannot delete: no ID provided');
+ }
$this->db->table($this->getTableName())
->delete()
->where($this->getIdField(), $id)
->execute();
}
- /** @return T[] */
+ /**
+ * Retrieves all records from the table.
+ *
+ * @return T[] Array of all entities.
+ */
public function findAll(): array {
$result = $this->db->table($this->getTableName())
->select()
@@ -47,8 +82,20 @@ public function findAll(): array {
return array_map(fn($row) => $this->toEntity($row), $result->fetchAll());
}
- /** @return T|null */
- public function findById(mixed $id): ?object {
+ /**
+ * Finds a single record by its ID.
+ *
+ * @param mixed $id The ID to search for, or null to use $this->id.
+ *
+ * @return T|null The entity if found, null otherwise.
+ *
+ * @throws \InvalidArgumentException 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 \InvalidArgumentException('Cannot find: no ID provided');
+ }
$result = $this->db->table($this->getTableName())
->select()
->where($this->getIdField(), $id)
@@ -57,7 +104,45 @@ public function findById(mixed $id): ?object {
return $result->getCount() > 0 ? $this->toEntity($result->fetch()) : null;
}
- /** @return Page */
+ /**
+ * Reloads $this or the given entity from the database.
+ *
+ * @param T|null $entity The entity to reload, or null to reload $this.
+ *
+ * @return T|null Fresh entity from database, or null if not found.
+ *
+ * @throws \InvalidArgumentException If no entity provided and $this has no ID.
+ */
+ public function reload(?object $entity = null): ?object {
+ if ($entity === null) {
+ return $this->findById();
+ }
+ $id = $this->toArray($entity)[$this->getIdField()] ?? null;
+ return $this->findById($id);
+ }
+
+ /**
+ * Gets the ID value from $this if it has entity properties.
+ *
+ * @return mixed The ID value or null.
+ */
+ private function getEntityId(): mixed {
+ $idField = $this->getIdField();
+ if (property_exists($this, $idField)) {
+ return $this->$idField;
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves paginated records using offset-based pagination.
+ *
+ * @param int $page Page number (1-based).
+ * @param int $perPage Number of records per page.
+ * @param array $orderBy Associative array of column => direction for sorting.
+ *
+ * @return Page Page object containing results and pagination metadata.
+ */
public function paginate(int $page = 1, int $perPage = 20, array $orderBy = []): Page {
$page = max(1, $page);
$offset = ($page - 1) * $perPage;
@@ -79,7 +164,16 @@ public function paginate(int $page = 1, int $perPage = 20, array $orderBy = []):
return new Page($items, $page, $perPage, $total);
}
- /** @return CursorPage */
+ /**
+ * Retrieves paginated records using cursor-based pagination.
+ *
+ * @param string|null $cursor Base64-encoded cursor value, null for first page.
+ * @param int $limit Maximum number of records to return.
+ * @param string|null $cursorField Column to use for cursor, defaults to ID field.
+ * @param string $direction Sort direction ('ASC' or 'DESC').
+ *
+ * @return CursorPage CursorPage object containing results and next cursor.
+ */
public function paginateByCursor(
?string $cursor = null,
int $limit = 20,
@@ -119,8 +213,21 @@ public function paginateByCursor(
return new CursorPage($items, $nextCursor, null, $hasMore);
}
- /** @param T $entity */
- public function save(object $entity): void {
+ /**
+ * Saves an entity (insert if new, update if existing).
+ *
+ * An entity is considered new if its ID field is null.
+ * If no entity is passed and $this has entity properties, saves $this.
+ *
+ * @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.
+ */
+ public function save(?object $entity = null): void {
+ if ($entity === null && !property_exists($this, $this->getIdField())) {
+ throw new \InvalidArgumentException('Cannot save: no entity provided');
+ }
+ $entity = $entity ?? $this;
$data = $this->toArray($entity);
$id = $data[$this->getIdField()] ?? null;
unset($data[$this->getIdField()]);
@@ -135,16 +242,99 @@ public function save(object $entity): void {
}
}
- protected function createQuery(): \WebFiori\Database\AbstractQuery {
+ /**
+ * Saves multiple entities in a single transaction.
+ *
+ * New entities (null ID) are batch inserted in one query.
+ * Existing entities are updated individually.
+ *
+ * @param T[] $entities Array of entities to save.
+ */
+ public function saveAll(array $entities): void {
+ if (empty($entities)) {
+ return;
+ }
+
+ $newEntities = [];
+ $existingEntities = [];
+ $idField = $this->getIdField();
+
+ foreach ($entities as $entity) {
+ $data = $this->toArray($entity);
+ if (($data[$idField] ?? null) === null) {
+ unset($data[$idField]);
+ $newEntities[] = $data;
+ } else {
+ $existingEntities[] = $data;
+ }
+ }
+
+ $this->db->transaction(function (Database $db) use ($newEntities, $existingEntities, $idField) {
+ if (!empty($newEntities)) {
+ $db->table($this->getTableName())->insert([
+ 'cols' => array_keys($newEntities[0]),
+ 'values' => array_map(fn($e) => array_values($e), $newEntities)
+ ])->execute();
+ }
+
+ foreach ($existingEntities as $data) {
+ $id = $data[$idField];
+ unset($data[$idField]);
+ $db->table($this->getTableName())
+ ->update($data)
+ ->where($idField, $id)
+ ->execute();
+ }
+ });
+ }
+
+ /**
+ * Creates a select query for the repository's table.
+ *
+ * @return AbstractQuery Query builder instance.
+ */
+ protected function createQuery(): AbstractQuery {
return $this->db->table($this->getTableName())->select();
}
+ /**
+ * Returns the underlying database instance.
+ *
+ * @return Database The database connection.
+ */
protected function getDatabase(): Database {
return $this->db;
}
+
+ /**
+ * Returns the name of the ID/primary key field.
+ *
+ * @return string Column name of the primary key.
+ */
abstract protected function getIdField(): string;
+ /**
+ * Returns the database table name for this repository.
+ *
+ * @return string Table name.
+ */
abstract protected function getTableName(): string;
+
+ /**
+ * Converts an entity to an associative array for database operations.
+ *
+ * @param T $entity The entity to convert.
+ *
+ * @return array Associative array with column names as keys.
+ */
abstract protected function toArray(object $entity): array;
+
+ /**
+ * Converts a database row to an entity object.
+ *
+ * @param array $row Associative array from database.
+ *
+ * @return T The mapped entity.
+ */
abstract protected function toEntity(array $row): object;
}
diff --git a/WebFiori/Database/Schema/SchemaChangeRepository.php b/WebFiori/Database/Schema/SchemaChangeRepository.php
index 5cbe894..3e6370a 100644
--- a/WebFiori/Database/Schema/SchemaChangeRepository.php
+++ b/WebFiori/Database/Schema/SchemaChangeRepository.php
@@ -40,7 +40,9 @@ public function __construct(Database $database) {
* @return int Number of records deleted
*/
public function clearAll(): int {
- return $this->deleteAll();
+ $count = $this->count();
+ $this->deleteAll();
+ return $count;
}
/**
diff --git a/WebFiori/Database/Schema/SchemaRunner.php b/WebFiori/Database/Schema/SchemaRunner.php
index fd1fb2c..c7f0509 100644
--- a/WebFiori/Database/Schema/SchemaRunner.php
+++ b/WebFiori/Database/Schema/SchemaRunner.php
@@ -275,7 +275,7 @@ public function createSchemaTable() {
* @param bool $recursive Whether to scan subdirectories recursively. Default is false.
* @return int Number of changes discovered and registered.
*/
- public function discoverFromPath(string $path, string $namespace, bool $recursive = false): int {
+ public function discoverFromPath(string $path, string $namespace = '', bool $recursive = false): int {
$count = 0;
if (!is_dir($path)) {
diff --git a/WebFiori/Database/Table.php b/WebFiori/Database/Table.php
index a27355d..72ee336 100644
--- a/WebFiori/Database/Table.php
+++ b/WebFiori/Database/Table.php
@@ -11,6 +11,7 @@
*/
namespace WebFiori\Database;
+use WebFiori\Database\Entity\EntityGenerator;
use WebFiori\Database\Entity\EntityMapper;
use WebFiori\Database\Factory\ColumnFactory;
use WebFiori\Database\MsSql\MSSQLTable;
diff --git a/examples/01-basic-connection/README.md b/examples/01-basic-connection/README.md
index f702061..64e7edb 100644
--- a/examples/01-basic-connection/README.md
+++ b/examples/01-basic-connection/README.md
@@ -10,7 +10,7 @@ This example demonstrates how to establish a connection to a database using the
## Files
-- `example.php` - Main example code
+- [`example.php`](example.php) - Main example code
## Running the Example
@@ -21,3 +21,10 @@ php example.php
## Expected Output
The example will output connection status and basic database information.
+
+
+## Related Examples
+
+- [02-basic-queries](../02-basic-queries/) - Learn CRUD operations after connecting
+- [03-table-blueprints](../03-table-blueprints/) - Create table structures programmatically
+- [06-migrations](../06-migrations/) - Manage schema changes with migrations
diff --git a/examples/02-basic-queries/README.md b/examples/02-basic-queries/README.md
index a18dc2e..02ef8e6 100644
--- a/examples/02-basic-queries/README.md
+++ b/examples/02-basic-queries/README.md
@@ -12,7 +12,7 @@ This example demonstrates CRUD operations (Create, Read, Update, Delete) using t
## Files
-- `example.php` - Main example code
+- [`example.php`](example.php) - Main example code
## Running the Example
@@ -23,3 +23,11 @@ php example.php
## Expected Output
The example will create a test table, perform various CRUD operations, and display the results of each operation.
+
+
+## Related Examples
+
+- [01-basic-connection](../01-basic-connection/) - Database connection setup
+- [03-table-blueprints](../03-table-blueprints/) - Define table structures
+- [05-transactions](../05-transactions/) - Wrap operations in transactions
+- [09-multi-result-queries](../09-multi-result-queries/) - Handle stored procedures with multiple result sets
diff --git a/examples/03-table-blueprints/README.md b/examples/03-table-blueprints/README.md
index 40877f2..ff318f9 100644
--- a/examples/03-table-blueprints/README.md
+++ b/examples/03-table-blueprints/README.md
@@ -12,8 +12,8 @@ This example demonstrates how to create database table structures using WebFiori
## Files
-- `example.php` - Main example code
-- `UserTable.php` - Custom table class extending MySQLTable
+- [`example.php`](example.php) - Main example code
+- [`UserTable.php`](UserTable.php) - Custom table class extending MySQLTable
## Running the Example
@@ -24,3 +24,11 @@ php example.php
## Expected Output
The example will create table blueprints using both the fluent API and custom table classes, generate SQL statements, and create the actual tables in the database.
+
+
+## Related Examples
+
+- [04-entity-mapping](../04-entity-mapping/) - Map tables to entity classes
+- [06-migrations](../06-migrations/) - Version control your schema changes
+- [10-attribute-based-tables](../10-attribute-based-tables/) - Define tables using PHP 8 attributes
+- [11-repository-pattern](../11-repository-pattern/) - Use repositories for data access
diff --git a/examples/03-table-blueprints/UserTable.php b/examples/03-table-blueprints/UserTable.php
index deb748e..554e605 100644
--- a/examples/03-table-blueprints/UserTable.php
+++ b/examples/03-table-blueprints/UserTable.php
@@ -29,15 +29,15 @@ public function __construct() {
ColOption::SIZE => 150,
ColOption::NULL => false
],
- 'full_name' => [
+ 'full-name' => [
ColOption::TYPE => DataType::VARCHAR,
ColOption::SIZE => 100
],
- 'is_active' => [
+ 'is-active' => [
ColOption::TYPE => DataType::BOOL,
ColOption::DEFAULT => true
],
- 'created_at' => [
+ 'created-at' => [
ColOption::TYPE => DataType::TIMESTAMP,
ColOption::DEFAULT => 'current_timestamp'
]
diff --git a/examples/03-table-blueprints/example.php b/examples/03-table-blueprints/example.php
index dda84c0..1bfa82d 100644
--- a/examples/03-table-blueprints/example.php
+++ b/examples/03-table-blueprints/example.php
@@ -41,7 +41,7 @@
]);
echo "✓ Users table blueprint created\n";
- echo " Columns: ".implode(', ', array_keys($usersTable->getColsNames()))."\n\n";
+ echo " Columns: ".implode(', ', array_keys($usersTable->getCols()))."\n\n";
echo "2. Creating Posts Table Blueprint:\n";
@@ -53,7 +53,7 @@
ColOption::PRIMARY => true,
ColOption::AUTO_INCREMENT => true
],
- 'user_id' => [
+ 'user-id' => [
ColOption::TYPE => DataType::INT,
ColOption::SIZE => 11,
ColOption::NULL => false
@@ -66,34 +66,34 @@
'content' => [
ColOption::TYPE => DataType::TEXT
],
- 'created_at' => [
+ 'created-at' => [
ColOption::TYPE => DataType::TIMESTAMP,
ColOption::DEFAULT => 'current_timestamp'
]
]);
echo "✓ Posts table blueprint created\n";
- echo " Columns: ".implode(', ', array_keys($postsTable->getColsNames()))."\n\n";
+ echo " Columns: ".implode(', ', array_keys($postsTable->getCols()))."\n\n";
echo "3. Adding Foreign Key Relationship:\n";
- // Add foreign key relationship
- $postsTable->addReference($usersTable, ['user_id'], 'user_fk');
+ // 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. Generating and Executing CREATE TABLE Statements:\n";
-
- // Generate create table queries
- $database->createTables();
+ echo "4. Creating Tables One by One:\n";
- // Show the generated SQL
- $sql = $database->getLastQuery();
- echo "Generated SQL:\n";
- echo str_replace(';', ";\n", $sql)."\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";
- // Execute the queries
+ // Create posts table (depends on users)
+ $database->table('posts')->createTable();
+ echo "SQL for posts table:\n".$database->getLastQuery()."\n\n";
$database->execute();
- echo "✓ Tables created successfully\n\n";
+ echo "✓ Posts table created\n\n";
echo "5. Testing the Created Tables:\n";
@@ -112,30 +112,25 @@
$userId = $userResult->getRows()[0]['id'];
$database->table('posts')->insert([
- 'user_id' => $userId,
+ 'user-id' => $userId,
'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->setQuery("
- SELECT u.username, p.title, p.created_at
- FROM users u
- JOIN posts p ON u.id = p.user_id
+ $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 "\nJoined data:\n";
-
foreach ($result as $row) {
echo " User: {$row['username']}, Post: {$row['title']}, Created: {$row['created_at']}\n";
}
- echo "\n6. Using Custom Table Class (Extending MySQLTable):\n";
-
- // Clean up previous tables first
- $database->setQuery("DROP TABLE IF EXISTS posts")->execute();
- $database->setQuery("DROP TABLE IF EXISTS users")->execute();
+ echo "\n6. Using Custom Table Class:\n";
// Include the custom table class
require_once __DIR__.'/UserTable.php';
@@ -150,31 +145,32 @@
// Generate and execute CREATE TABLE for custom table
$createQuery = $customTable->toSQL();
- echo "\nGenerated SQL for custom table:\n";
- echo $createQuery."\n\n";
+ echo "\nGenerated SQL for custom table:\n$createQuery\n\n";
// Execute the custom table creation
- $database->setQuery($createQuery)->execute();
+ $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'
+ '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 "\n7. Final Cleanup:\n";
- $database->setQuery("DROP TABLE users_extended")->execute();
+ 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";
diff --git a/examples/04-entity-mapping/README.md b/examples/04-entity-mapping/README.md
index 2f6dca8..d08e21c 100644
--- a/examples/04-entity-mapping/README.md
+++ b/examples/04-entity-mapping/README.md
@@ -11,7 +11,7 @@ This example demonstrates how to create and use entity classes for object-relati
## Files
-- `example.php` - Main example code
+- [`example.php`](example.php) - Main example code
- `User.php` - Generated entity class (created during execution)
## Running the Example
@@ -23,3 +23,10 @@ php example.php
## Expected Output
The example will create a table blueprint, generate an entity class, and demonstrate mapping database records to objects.
+
+
+## Related Examples
+
+- [03-table-blueprints](../03-table-blueprints/) - Create table structures first
+- [11-repository-pattern](../11-repository-pattern/) - Recommended approach for entity mapping
+- [12-clean-architecture](../12-clean-architecture/) - Separate domain entities from infrastructure
diff --git a/examples/04-entity-mapping/example.php b/examples/04-entity-mapping/example.php
index e2190ab..ca9f299 100644
--- a/examples/04-entity-mapping/example.php
+++ b/examples/04-entity-mapping/example.php
@@ -9,159 +9,97 @@
echo "=== WebFiori Database Entity Mapping Example ===\n\n";
+echo "This example shows entity generation and manual mapping approaches.\n\n";
+
try {
- // Create connection
$connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql');
$database = new Database($connection);
- echo "1. Creating User Table Blueprint:\n";
+ echo "1. Creating User Table:\n";
- // Create user table blueprint
$userTable = $database->createBlueprint('users')->addColumns([
- 'id' => [
- ColOption::TYPE => DataType::INT,
- ColOption::SIZE => 11,
- ColOption::PRIMARY => true,
- ColOption::AUTO_INCREMENT => true
- ],
- 'first_name' => [
- ColOption::TYPE => DataType::VARCHAR,
- ColOption::SIZE => 50,
- ColOption::NULL => false
- ],
- 'last_name' => [
- ColOption::TYPE => DataType::VARCHAR,
- ColOption::SIZE => 50,
- ColOption::NULL => false
- ],
- 'email' => [
- ColOption::TYPE => DataType::VARCHAR,
- ColOption::SIZE => 150,
- ColOption::NULL => false
- ],
- 'age' => [
- ColOption::TYPE => DataType::INT,
- ColOption::SIZE => 3
- ]
+ 'id' => [ColOption::TYPE => DataType::INT, ColOption::PRIMARY => true, ColOption::AUTO_INCREMENT => true],
+ '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]
]);
- echo "✓ User table blueprint created\n\n";
-
- echo "2. Creating Entity Class:\n";
-
- // Get entity mapper and create entity class
- $entityMapper = $userTable->getEntityMapper();
- $entityMapper->setEntityName('User');
- $entityMapper->setNamespace('');
- $entityMapper->setPath(__DIR__);
-
- // Create the entity class
- $entityMapper->create();
- echo "✓ User entity class created at: ".__DIR__."/User.php\n";
-
-
- echo "3. Creating Table in Database:\n";
-
- // Create the table
- $database->createTables();
+ $database->table('users')->createTable();
$database->execute();
- echo "✓ User table created in database\n\n";
-
- echo "4. Inserting Test Data:\n";
-
- // Insert test users
- $database->table('users')->insert([
- 'first_name' => 'Khalid',
- 'last_name' => 'Al-Rashid',
- 'email' => 'khalid.rashid@example.com',
- 'age' => 30
- ])->execute();
-
- $database->table('users')->insert([
- 'first_name' => 'Aisha',
- 'last_name' => 'Mahmoud',
- 'email' => 'aisha.mahmoud@example.com',
- 'age' => 25
- ])->execute();
-
- $database->table('users')->insert([
- 'first_name' => 'Hassan',
- 'last_name' => 'Al-Najjar',
- 'email' => 'hassan.najjar@example.com',
- 'age' => 35
- ])->execute();
+ echo "✓ User table created\n\n";
+
+ 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";
- echo "5. Fetching and Mapping Records:\n";
+ // ============================================
+ // APPROACH 1: Using EntityMapper (Deprecated)
+ // ============================================
+ echo "3. Using EntityGenerator:\n";
+
+ $entityGenerator = $userTable->getEntityGenerator('User', __DIR__, '');
+ $entityGenerator->generate();
+ echo "✓ User entity class generated at: ".__DIR__."/User.php\n";
- // Include the generated entity class
require_once __DIR__.'/User.php';
- // Fetch records and map to objects
$resultSet = $database->table('users')->select()->execute();
-
- $mappedUsers = $resultSet->map(function (array $record)
- {
- return User::map($record);
- });
-
- echo "Mapped users as objects:\n";
-
+ $mappedUsers = $resultSet->map(fn($record) => new User(
+ id: (int) $record['id'],
+ firstName: $record['first_name'],
+ lastName: $record['last_name'],
+ email: $record['email'],
+ age: (int) $record['age']
+ ));
+
+ echo "Mapped users (EntityGenerator):\n";
foreach ($mappedUsers as $user) {
- echo " - {$user->getFirstName()} {$user->getLastName()} ({$user->getEmail()}) - Age: {$user->getAge()}\n";
+ echo " - {$user->getFirstName()} {$user->getLastName()} ({$user->getEmail()})\n";
}
echo "\n";
- echo "6. Working with Individual Objects:\n";
-
- // Get first user and demonstrate object methods
- $firstUser = $mappedUsers->getRows()[0];
- echo "First user details:\n";
- echo " ID: {$firstUser->getId()}\n";
- echo " Full Name: {$firstUser->getFirstName()} {$firstUser->getLastName()}\n";
- echo " Email: {$firstUser->getEmail()}\n";
- echo " Age: {$firstUser->getAge()}\n\n";
-
- echo "7. Filtering with Entity Objects:\n";
-
- // Filter users by age
- $adultUsers = [];
-
- foreach ($mappedUsers as $user) {
- if ($user->getAge() >= 30) {
- $adultUsers[] = $user;
- }
- }
-
- echo "Users 30 or older:\n";
-
- foreach ($adultUsers as $user) {
- echo " - {$user->getFirstName()} {$user->getLastName()} (Age: {$user->getAge()})\n";
+ // ============================================
+ // APPROACH 2: Manual Entity (Recommended)
+ // ============================================
+ echo "4. Alternative: 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 "\n";
- echo "\n8. Cleanup:\n";
- $database->setQuery("DROP TABLE users")->execute();
+ echo "5. Cleanup:\n";
+ $database->raw("DROP TABLE users")->execute();
echo "✓ User table dropped\n";
- // Clean up generated file
if (file_exists(__DIR__.'/User.php')) {
unlink(__DIR__.'/User.php');
echo "✓ Generated User.php file removed\n";
}
} catch (Exception $e) {
echo "✗ Error: ".$e->getMessage()."\n";
-
- // Clean up on error
try {
- $database->setQuery("DROP TABLE IF EXISTS users")->execute();
-
- if (file_exists(__DIR__.'/User.php')) {
- unlink(__DIR__.'/User.php');
- }
- } catch (Exception $cleanupError) {
- // Ignore cleanup errors
- }
+ $database->raw("DROP TABLE IF EXISTS users")->execute();
+ if (file_exists(__DIR__.'/User.php')) unlink(__DIR__.'/User.php');
+ } catch (Exception $cleanupError) {}
}
echo "\n=== Example Complete ===\n";
diff --git a/examples/05-transactions/README.md b/examples/05-transactions/README.md
index 8533a6f..1ab3699 100644
--- a/examples/05-transactions/README.md
+++ b/examples/05-transactions/README.md
@@ -11,7 +11,7 @@ This example demonstrates how to use database transactions to ensure data integr
## Files
-- `example.php` - Main example code
+- [`example.php`](example.php) - Main example code
## Running the Example
@@ -22,3 +22,10 @@ php example.php
## Expected Output
The example will demonstrate both successful transactions and failed transactions with rollback functionality.
+
+
+## Related Examples
+
+- [02-basic-queries](../02-basic-queries/) - Basic CRUD operations
+- [06-migrations](../06-migrations/) - Schema changes with rollback support
+- [07-seeders](../07-seeders/) - Populate data within transactions
diff --git a/examples/06-migrations/AddEmailIndexMigration.php b/examples/06-migrations/AddEmailIndexMigration.php
index 5d723f5..c165f71 100644
--- a/examples/06-migrations/AddEmailIndexMigration.php
+++ b/examples/06-migrations/AddEmailIndexMigration.php
@@ -5,47 +5,17 @@
/**
* Migration to add a unique index on the email column.
- *
- * This migration adds a unique constraint to the email column
- * in the users table to ensure email uniqueness across all users.
- * This migration depends on the users table existing.
*/
class AddEmailIndexMigration extends AbstractMigration {
- /**
- * Rollback the migration changes from the database.
- *
- * Removes the unique index from the email column,
- * allowing duplicate emails again.
- *
- * @param Database $db The database instance to execute rollback on.
- */
public function down(Database $db): void {
- // Drop email index
- $db->setQuery("ALTER TABLE users DROP INDEX idx_users_email")->execute();
+ $db->raw("ALTER TABLE users DROP INDEX idx_users_email")->execute();
}
- /**
- * Get the list of migration dependencies.
- *
- * This migration requires the users table to exist before
- * it can add an index to the email column.
- *
- * @return array Array of migration names this migration depends on.
- */
public function getDependencies(): array {
return ['CreateUsersTableMigration'];
}
- /**
- * Apply the migration changes to the database.
- *
- * Adds a unique index on the email column to enforce
- * email uniqueness and improve query performance.
- *
- * @param Database $db The database instance to execute changes on.
- */
public function up(Database $db): void {
- // Add unique index on email column
- $db->setQuery("ALTER TABLE users ADD UNIQUE INDEX idx_users_email (email)")->execute();
+ $db->raw("ALTER TABLE users ADD UNIQUE INDEX idx_users_email (email)")->execute();
}
}
diff --git a/examples/06-migrations/CreateUsersTableMigration.php b/examples/06-migrations/CreateUsersTableMigration.php
index d04800b..c832de0 100644
--- a/examples/06-migrations/CreateUsersTableMigration.php
+++ b/examples/06-migrations/CreateUsersTableMigration.php
@@ -7,35 +7,13 @@
/**
* Migration to create the users table.
- *
- * This migration creates a basic users table with essential columns
- * for user management including auto-incrementing ID, username, email,
- * password hash, and creation timestamp.
*/
class CreateUsersTableMigration extends AbstractMigration {
- /**
- * Rollback the migration changes from the database.
- *
- * Drops the users table and all its data. This operation
- * is irreversible and will result in data loss.
- *
- * @param Database $db The database instance to execute rollback on.
- */
public function down(Database $db): void {
- // Drop users table
- $db->setQuery("DROP TABLE IF EXISTS users")->execute();
+ $db->raw("DROP TABLE IF EXISTS users")->execute();
}
- /**
- * Apply the migration changes to the database.
- *
- * Creates the users table with columns for user authentication
- * and basic profile information.
- *
- * @param Database $db The database instance to execute changes on.
- */
public function up(Database $db): void {
- // Create users table
$db->createBlueprint('users')->addColumns([
'id' => [
ColOption::TYPE => DataType::INT,
@@ -53,18 +31,18 @@ public function up(Database $db): void {
ColOption::SIZE => 150,
ColOption::NULL => false
],
- 'password_hash' => [
+ 'password-hash' => [
ColOption::TYPE => DataType::VARCHAR,
ColOption::SIZE => 255,
ColOption::NULL => false
],
- 'created_at' => [
+ 'created-at' => [
ColOption::TYPE => DataType::TIMESTAMP,
ColOption::DEFAULT => 'current_timestamp'
]
]);
- $db->createTables();
+ $db->table('users')->createTable();
$db->execute();
}
}
diff --git a/examples/06-migrations/README.md b/examples/06-migrations/README.md
index 98c62ab..aec4f2f 100644
--- a/examples/06-migrations/README.md
+++ b/examples/06-migrations/README.md
@@ -11,9 +11,9 @@ This example demonstrates how to create and run database migrations using WebFio
## Files
-- `example.php` - Main example code
-- `CreateUsersTableMigration.php` - Migration to create users table
-- `AddEmailIndexMigration.php` - Migration to add email index
+- [`example.php`](example.php) - Main example code
+- [`CreateUsersTableMigration.php`](CreateUsersTableMigration.php) - Migration to create users table
+- [`AddEmailIndexMigration.php`](AddEmailIndexMigration.php) - Migration to add email index
## Running the Example
@@ -24,3 +24,10 @@ php example.php
## Expected Output
The example will create migration classes, run them to modify the database schema, and demonstrate rollback functionality.
+
+
+## Related Examples
+
+- [03-table-blueprints](../03-table-blueprints/) - Define table structures
+- [07-seeders](../07-seeders/) - Populate data after migrations
+- [05-transactions](../05-transactions/) - Understand rollback behavior
diff --git a/examples/06-migrations/example.php b/examples/06-migrations/example.php
index d316f31..6492b3b 100644
--- a/examples/06-migrations/example.php
+++ b/examples/06-migrations/example.php
@@ -13,130 +13,115 @@
$connection = new ConnectionInfo('mysql', 'root', '123456', 'mysql');
$database = new Database($connection);
- echo "1. Loading Migration Classes:\n";
-
- // Include migration classes
- require_once __DIR__.'/CreateUsersTableMigration.php';
- require_once __DIR__.'/AddEmailIndexMigration.php';
-
- echo "✓ Migration classes loaded\n";
-
- echo "2. Setting up Schema Runner:\n";
+ echo "1. Setting up Schema Runner:\n";
// Create schema runner
$runner = new SchemaRunner($connection);
- // Register migration classes
- $runner->register('CreateUsersTableMigration');
- $runner->register('AddEmailIndexMigration');
+ // Discover and register migration classes from directory
+ $runner->discoverFromPath(__DIR__, '');
echo "✓ Schema runner created\n";
- echo "✓ Migration classes registered\n";
+ echo "✓ Migration classes discovered from path\n";
// Create schema tracking table
$runner->createSchemaTable();
echo "✓ Schema tracking table created\n\n";
- echo "3. Checking Available Migrations:\n";
+ echo "2. Checking Available Migrations:\n";
$changes = $runner->getChanges();
- echo "Registered migrations:\n";
-
+ echo "Discovered migrations:\n";
foreach ($changes as $change) {
echo " - ".$change->getName()."\n";
}
echo "\n";
- echo "4. Running Migrations:\n";
+ echo "3. Running Migrations (using apply()):\n";
- // Force apply all migrations
- $changes = $runner->getChanges();
- $appliedChanges = [];
+ // Apply all pending migrations
+ $result = $runner->apply();
- foreach ($changes as $change) {
- if (!$runner->isApplied($change->getName())) {
- $change->execute($database);
- $appliedChanges[] = $change;
- echo " ✓ Applied: ".$change->getName()."\n";
+ if ($result->count() > 0) {
+ echo "Applied migrations:\n";
+ foreach ($result->getApplied() as $change) {
+ echo " ✓ ".$change->getName()."\n";
}
+ } else {
+ echo "No migrations to apply (all up to date)\n";
}
- if (empty($appliedChanges)) {
- 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 "\n";
- echo "5. Verifying Database Structure:\n";
+ echo "4. Verifying Database Structure:\n";
// Check if table exists
- $result = $database->setQuery("SHOW TABLES LIKE 'users'")->execute();
-
- if ($result->getRowsCount() > 0) {
+ $tableResult = $database->raw("SHOW TABLES LIKE 'users'")->execute();
+ if ($tableResult->getRowsCount() > 0) {
echo "✓ Users table created\n";
}
// Check table structure
- $result = $database->setQuery("DESCRIBE users")->execute();
+ $descResult = $database->raw("DESCRIBE users")->execute();
echo "Users table columns:\n";
-
- foreach ($result as $column) {
+ foreach ($descResult as $column) {
echo " - {$column['Field']} ({$column['Type']})\n";
}
// Check indexes
- $result = $database->setQuery("SHOW INDEX FROM users WHERE Key_name = 'idx_users_email'")->execute();
-
- if ($result->getRowsCount() > 0) {
+ $indexResult = $database->raw("SHOW INDEX FROM users WHERE Key_name = 'idx_users_email'")->execute();
+ if ($indexResult->getRowsCount() > 0) {
echo "✓ Email index created\n";
}
echo "\n";
- echo "6. Testing Data Operations:\n";
+ 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)
+ '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)
+ 'password-hash' => password_hash('password456', PASSWORD_DEFAULT)
])->execute();
echo "✓ Test users inserted\n";
// Query data
- $result = $database->table('users')->select(['username', 'email', 'created_at'])->execute();
+ $selectResult = $database->table('users')->select(['username', 'email', 'created-at'])->execute();
echo "Inserted users:\n";
-
- foreach ($result as $user) {
+ foreach ($selectResult as $user) {
echo " - {$user['username']} ({$user['email']}) - {$user['created_at']}\n";
}
echo "\n";
- echo "7. Checking Migration Status:\n";
-
- // Check which migrations are applied
+ 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 "8. Rolling Back Migrations:\n";
+ echo "7. Rolling Back Migrations:\n";
// Rollback all migrations
- $rolledBackChanges = $runner->rollbackUpTo(null);
+ $rolledBack = $runner->rollbackUpTo(null);
- if (!empty($rolledBackChanges)) {
+ if (!empty($rolledBack)) {
echo "Rolled back migrations:\n";
-
- foreach ($rolledBackChanges as $change) {
+ foreach ($rolledBack as $change) {
echo " ✓ ".$change->getName()."\n";
}
} else {
@@ -144,13 +129,12 @@
}
// Verify rollback
- $result = $database->setQuery("SHOW TABLES LIKE 'users'")->execute();
-
- if ($result->getRowsCount() == 0) {
+ $verifyResult = $database->raw("SHOW TABLES LIKE 'users'")->execute();
+ if ($verifyResult->getRowsCount() == 0) {
echo "✓ Users table removed\n";
}
- echo "\n9. Cleanup:\n";
+ echo "\n8. Cleanup:\n";
$runner->dropSchemaTable();
echo "✓ Schema tracking table dropped\n";
} catch (Exception $e) {
@@ -158,8 +142,8 @@
// Clean up on error
try {
- $database->setQuery("DROP TABLE IF EXISTS users")->execute();
- $database->setQuery("DROP TABLE IF EXISTS schema_changes")->execute();
+ $database->raw("DROP TABLE IF EXISTS users")->execute();
+ $database->raw("DROP TABLE IF EXISTS schema_changes")->execute();
} catch (Exception $cleanupError) {
// Ignore cleanup errors
}
diff --git a/examples/07-seeders/CategoriesSeeder.php b/examples/07-seeders/CategoriesSeeder.php
index bfda645..3a06cd7 100644
--- a/examples/07-seeders/CategoriesSeeder.php
+++ b/examples/07-seeders/CategoriesSeeder.php
@@ -5,56 +5,18 @@
/**
* Seeder for populating the categories table with sample category data.
- *
- * This seeder is environment-specific and only runs in development
- * and test environments to provide sample categories for testing.
*/
class CategoriesSeeder extends AbstractSeeder {
- /**
- * Get the environments where this seeder should be executed.
- *
- * This seeder only runs in development and test environments
- * to avoid populating production with sample data.
- *
- * @return array Array of environment names where this seeder should run.
- */
public function getEnvironments(): array {
- // Only run in development and test environments
return ['dev', 'test'];
}
- /**
- * Run the seeder to populate the database with data.
- *
- * Inserts sample categories for content organization including
- * technology, science, culture, and sports categories.
- *
- * @param Database $db The database instance to execute seeding on.
- * @return bool True if seeding was successful, false otherwise.
- */
public function run(Database $db): void {
- // Insert sample categories
$categories = [
- [
- 'name' => 'Technology',
- 'description' => 'Articles about technology and programming',
- 'slug' => 'technology'
- ],
- [
- 'name' => 'Science',
- 'description' => 'Scientific articles and research',
- 'slug' => 'science'
- ],
- [
- 'name' => 'Culture',
- 'description' => 'Cultural topics and discussions',
- 'slug' => 'culture'
- ],
- [
- 'name' => 'Sports',
- 'description' => 'Sports news and updates',
- 'slug' => 'sports'
- ]
+ ['name' => 'Technology', 'description' => 'Articles about technology', 'slug' => 'technology'],
+ ['name' => 'Science', 'description' => 'Scientific articles', 'slug' => 'science'],
+ ['name' => 'Culture', 'description' => 'Cultural topics', 'slug' => 'culture'],
+ ['name' => 'Sports', 'description' => 'Sports news', 'slug' => 'sports']
];
foreach ($categories as $category) {
diff --git a/examples/07-seeders/README.md b/examples/07-seeders/README.md
index edf7554..648901e 100644
--- a/examples/07-seeders/README.md
+++ b/examples/07-seeders/README.md
@@ -11,9 +11,9 @@ This example demonstrates how to create and run database seeders using WebFiori'
## Files
-- `example.php` - Main example code
-- `UsersSeeder.php` - Seeder for user data
-- `CategoriesSeeder.php` - Seeder for category data
+- [`example.php`](example.php) - Main example code
+- [`UsersSeeder.php`](UsersSeeder.php) - Seeder for user data
+- [`CategoriesSeeder.php`](CategoriesSeeder.php) - Seeder for category data
## Running the Example
@@ -24,3 +24,10 @@ php example.php
## Expected Output
The example will create seeder classes, run them to populate the database with initial data, and show the seeded records.
+
+
+## Related Examples
+
+- [06-migrations](../06-migrations/) - Create tables before seeding
+- [05-transactions](../05-transactions/) - Wrap seeding in transactions
+- [11-repository-pattern](../11-repository-pattern/) - Use repositories for data insertion
diff --git a/examples/07-seeders/UsersSeeder.php b/examples/07-seeders/UsersSeeder.php
index 332a00d..5e02311 100644
--- a/examples/07-seeders/UsersSeeder.php
+++ b/examples/07-seeders/UsersSeeder.php
@@ -5,45 +5,32 @@
/**
* Seeder for populating the users table with initial user data.
- *
- * This seeder creates essential user accounts including an administrator
- * and sample users for development and testing purposes.
*/
class UsersSeeder extends AbstractSeeder {
- /**
- * Run the seeder to populate the database with data.
- *
- * Inserts sample user accounts with different roles including
- * an administrator account and regular users with Arabic names.
- *
- * @param Database $db The database instance to execute seeding on.
- * @return bool True if seeding was successful, false otherwise.
- */
public function run(Database $db): void {
- // Insert sample users
$users = [
[
'username' => 'admin',
'email' => 'admin@example.com',
- 'full_name' => 'Administrator',
+ 'full-name' => 'Administrator',
'role' => 'admin'
],
[
'username' => 'mohammed_ali',
'email' => 'mohammed@example.com',
- 'full_name' => 'Mohammed Ali Al-Rashid',
+ 'full-name' => 'Mohammed Ali Al-Rashid',
'role' => 'user'
],
[
'username' => 'zahra_hassan',
'email' => 'zahra@example.com',
- 'full_name' => 'Zahra Hassan Al-Mahmoud',
+ 'full-name' => 'Zahra Hassan Al-Mahmoud',
'role' => 'user'
],
[
'username' => 'omar_khalil',
'email' => 'omar@example.com',
- 'full_name' => 'Omar Khalil Al-Najjar',
+ 'full-name' => 'Omar Khalil Al-Najjar',
'role' => 'moderator'
]
];
diff --git a/examples/07-seeders/example.php b/examples/07-seeders/example.php
index de32545..5b462f9 100644
--- a/examples/07-seeders/example.php
+++ b/examples/07-seeders/example.php
@@ -18,8 +18,8 @@
echo "1. Creating Test Tables:\n";
// Clean up any existing tables first
- $database->setQuery("DROP TABLE IF EXISTS categories")->execute();
- $database->setQuery("DROP TABLE IF EXISTS users")->execute();
+ $database->raw("DROP TABLE IF EXISTS categories")->execute();
+ $database->raw("DROP TABLE IF EXISTS users")->execute();
// Create users table
$database->createBlueprint('users')->addColumns([
@@ -39,7 +39,7 @@
ColOption::SIZE => 150,
ColOption::NULL => false
],
- 'full_name' => [
+ 'full-name' => [
ColOption::TYPE => DataType::VARCHAR,
ColOption::SIZE => 100
],
@@ -48,7 +48,7 @@
ColOption::SIZE => 20,
ColOption::DEFAULT => 'user'
],
- 'is_active' => [
+ 'is-active' => [
ColOption::TYPE => DataType::BOOL,
ColOption::DEFAULT => true
]
@@ -77,132 +77,116 @@
]
]);
- $database->createTables();
+ // Create tables one by one
+ $database->table('users')->createTable();
$database->execute();
+ echo "✓ Users table created\n";
- echo "✓ Test tables created\n\n";
-
- echo "2. Loading Seeder Classes:\n";
-
- // Include seeder classes
- require_once __DIR__.'/UsersSeeder.php';
- require_once __DIR__.'/CategoriesSeeder.php';
-
- echo "✓ Seeder classes loaded\n";
+ $database->table('categories')->createTable();
+ $database->execute();
+ echo "✓ Categories table created\n\n";
- echo "3. Setting up Schema Runner:\n";
+ echo "2. Setting up Schema Runner:\n";
// Create schema runner
$runner = new SchemaRunner($connection);
- // Register seeder classes
- $runner->register('UsersSeeder');
- $runner->register('CategoriesSeeder');
+ // Discover and register seeder classes from directory
+ $runner->discoverFromPath(__DIR__, '');
echo "✓ Schema runner created\n";
- echo "✓ Seeder classes registered\n";
+ echo "✓ Seeder classes discovered from path\n";
// Create schema tracking table
$runner->createSchemaTable();
echo "✓ Schema tracking table created\n\n";
- echo "4. Checking Available Seeders:\n";
+ echo "3. Checking Available Seeders:\n";
$changes = $runner->getChanges();
- echo "Registered seeders:\n";
-
+ echo "Discovered seeders:\n";
foreach ($changes as $change) {
echo " - ".$change->getName()."\n";
}
echo "\n";
- echo "5. Running Seeders:\n";
+ echo "4. Running Seeders (using apply()):\n";
- // Force apply all seeders
- $appliedChanges = [];
+ // Apply all pending seeders
+ $result = $runner->apply();
- foreach ($changes as $change) {
- if (!$runner->isApplied($change->getName())) {
- $change->execute($database);
- $appliedChanges[] = $change;
- echo " ✓ Applied: ".$change->getName()."\n";
+ if ($result->count() > 0) {
+ echo "Applied seeders:\n";
+ foreach ($result->getApplied() as $change) {
+ echo " ✓ ".$change->getName()."\n";
}
- }
-
- if (empty($appliedChanges)) {
+ } else {
echo "No seeders to apply (all up to date)\n";
}
echo "\n";
- echo "6. Verifying Seeded Data:\n";
+ echo "5. Verifying Seeded Data:\n";
// Check users data
- $result = $database->table('users')->select()->execute();
- echo "Seeded users ({$result->getRowsCount()} records):\n";
-
- foreach ($result as $user) {
+ $usersResult = $database->table('users')->select()->execute();
+ echo "Seeded 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']} - $status\n";
}
echo "\n";
// Check categories data
- $result = $database->table('categories')->select()->execute();
- echo "Seeded categories ({$result->getRowsCount()} records):\n";
-
- foreach ($result as $category) {
+ $categoriesResult = $database->table('categories')->select()->execute();
+ echo "Seeded categories ({$categoriesResult->getRowsCount()} records):\n";
+ foreach ($categoriesResult as $category) {
echo " - {$category['name']} ({$category['slug']})\n";
- echo " {$category['description']}\n";
}
echo "\n";
- echo "7. Testing Seeder Status:\n";
-
- // Check which seeders are applied
+ 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 "\n";
- echo "8. Rolling Back Seeders:\n";
-
- // Rollback all seeders (this will clear the data)
- $rolledBackChanges = [];
+ echo "7. Rolling Back Seeders:\n";
- // Reverse order for rollback
- $reversedChanges = array_reverse($changes);
+ // Rollback all seeders (note: seeders don't clear data by default)
+ $rolledBack = $runner->rollbackUpTo(null);
- foreach ($reversedChanges as $change) {
- $change->rollback($database);
- $rolledBackChanges[] = $change;
- echo " ✓ Rolled back: ".$change->getName()."\n";
+ 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";
}
- // Verify rollback
+ // 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 "After rollback:\n";
+ echo "Note: Data remains after rollback (seeders don't clear data by default):\n";
echo " Users: $userCount records\n";
- echo " Categories: $categoryCount records\n";
- echo "✓ Seeders rolled back successfully\n\n";
+ echo " Categories: $categoryCount records\n\n";
- echo "9. Cleanup:\n";
+ echo "8. Cleanup:\n";
$runner->dropSchemaTable();
- $database->setQuery("DROP TABLE categories")->execute();
- $database->setQuery("DROP TABLE users")->execute();
+ $database->raw("DROP TABLE categories")->execute();
+ $database->raw("DROP TABLE users")->execute();
echo "✓ Test tables and schema tracking table dropped\n";
} catch (Exception $e) {
echo "✗ Error: ".$e->getMessage()."\n";
// Clean up on error
try {
- $database->setQuery("DROP TABLE IF EXISTS categories")->execute();
- $database->setQuery("DROP TABLE IF EXISTS users")->execute();
- $database->setQuery("DROP TABLE IF EXISTS schema_changes")->execute();
+ $database->raw("DROP TABLE IF EXISTS categories")->execute();
+ $database->raw("DROP TABLE IF EXISTS users")->execute();
+ $database->raw("DROP TABLE IF EXISTS schema_changes")->execute();
} catch (Exception $cleanupError) {
// Ignore cleanup errors
}
diff --git a/examples/08-performance-monitoring/README.md b/examples/08-performance-monitoring/README.md
index 87de142..71a9aca 100644
--- a/examples/08-performance-monitoring/README.md
+++ b/examples/08-performance-monitoring/README.md
@@ -12,7 +12,7 @@ This example demonstrates how to monitor database query performance using WebFio
## Files
-- `example.php` - Main example code
+- [`example.php`](example.php) - Main example code
## Running the Example
@@ -23,3 +23,10 @@ php example.php
## Expected Output
The example will execute various database operations while monitoring performance, then display detailed performance metrics and analysis.
+
+
+## Related Examples
+
+- [02-basic-queries](../02-basic-queries/) - Queries to monitor
+- [09-multi-result-queries](../09-multi-result-queries/) - Complex queries to analyze
+- [13-pagination](../13-pagination/) - Optimize paginated queries
diff --git a/examples/09-multi-result-queries/README.md b/examples/09-multi-result-queries/README.md
new file mode 100644
index 0000000..a9870be
--- /dev/null
+++ b/examples/09-multi-result-queries/README.md
@@ -0,0 +1,30 @@
+# Multi-Result Queries
+
+This example demonstrates how to handle stored procedures and queries that return multiple result sets.
+
+## What This Example Shows
+
+- Creating stored procedures with multiple SELECT statements
+- Executing multi-result queries
+- Working with `MultiResultSet` objects
+- Processing individual result sets
+
+## Files
+
+- [`example.php`](example.php) - Main example code
+
+## Running the Example
+
+```bash
+php example.php
+```
+
+## Expected Output
+
+The example will create stored procedures, execute them, and demonstrate how to iterate through multiple result sets.
+
+## Related Examples
+
+- [02-basic-queries](../02-basic-queries/) - Basic query operations
+- [05-transactions](../05-transactions/) - Combine with transactions
+- [08-performance-monitoring](../08-performance-monitoring/) - Monitor complex query performance
diff --git a/examples/10-attribute-based-tables/Article.php b/examples/10-attribute-based-tables/Article.php
new file mode 100644
index 0000000..b2ff791
--- /dev/null
+++ b/examples/10-attribute-based-tables/Article.php
@@ -0,0 +1,25 @@
+getCols()))."\n";
+
+ echo "✓ Articles table blueprint created\n";
+ echo " Columns: ".implode(', ', array_keys($articlesTable->getCols()))."\n\n";
+
+ 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";
+
+ $database->raw("DROP TABLE IF EXISTS articles")->execute();
+ $database->raw("DROP TABLE IF EXISTS authors")->execute();
+
+ $database->raw($authorsTable->toSQL())->execute();
+ echo "✓ Authors table created\n";
+
+ $database->raw($articlesTable->toSQL())->execute();
+ echo "✓ Articles table created\n\n";
+
+ 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";
+
+ $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 "5. Querying Data:\n";
+
+ $result = $database->raw("
+ SELECT a.name as author, ar.title, ar.`published-at`
+ FROM authors a JOIN articles ar ON a.id = ar.`author-id`
+ ORDER BY ar.`published-at` DESC
+ ")->execute();
+
+ echo "Articles with authors:\n";
+ foreach ($result as $row) {
+ echo " - {$row['title']} by {$row['author']} ({$row['published-at']})\n";
+ }
+ echo "\n";
+
+ echo "6. Cleanup:\n";
+ $database->raw("DROP TABLE articles")->execute();
+ $database->raw("DROP TABLE authors")->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();
+ } catch (Exception $cleanupError) {}
+}
+
+echo "\n=== Example Complete ===\n";
diff --git a/examples/11-repository-pattern/Product.php b/examples/11-repository-pattern/Product.php
new file mode 100644
index 0000000..6848bbc
--- /dev/null
+++ b/examples/11-repository-pattern/Product.php
@@ -0,0 +1,16 @@
+name = $name;
+ $this->category = $category;
+ $this->price = $price;
+ $this->stock = $stock;
+ }
+}
diff --git a/examples/11-repository-pattern/ProductRepository.php b/examples/11-repository-pattern/ProductRepository.php
new file mode 100644
index 0000000..def5731
--- /dev/null
+++ b/examples/11-repository-pattern/ProductRepository.php
@@ -0,0 +1,53 @@
+id = (int) $row['id'];
+ $product->name = $row['name'];
+ $product->category = $row['category'];
+ $product->price = (float) $row['price'];
+ $product->stock = (int) $row['stock'];
+ return $product;
+ }
+
+ protected function toArray(object $entity): array {
+ return [
+ 'id' => $entity->id,
+ 'name' => $entity->name,
+ 'category' => $entity->category,
+ 'price' => $entity->price,
+ 'stock' => $entity->stock
+ ];
+ }
+
+ public function findByCategory(string $category): array {
+ $result = $this->getDatabase()->table($this->getTableName())
+ ->select()
+ ->where('category', $category)
+ ->execute();
+
+ return array_map(fn($row) => $this->toEntity($row), $result->fetchAll());
+ }
+
+ public function findLowStock(int $threshold = 10): array {
+ $result = $this->getDatabase()->table($this->getTableName())
+ ->select()
+ ->where('stock', $threshold, '<')
+ ->execute();
+
+ return array_map(fn($row) => $this->toEntity($row), $result->fetchAll());
+ }
+}
diff --git a/examples/11-repository-pattern/README.md b/examples/11-repository-pattern/README.md
new file mode 100644
index 0000000..7e76345
--- /dev/null
+++ b/examples/11-repository-pattern/README.md
@@ -0,0 +1,58 @@
+# Repository Pattern
+
+This example demonstrates how to use the Repository pattern with `AbstractRepository` for data access.
+
+## Entity + Repository = Model
+
+In traditional MVC frameworks, a "Model" class often combines data and database logic together (Active Record pattern). The Repository pattern separates these into two focused classes:
+
+| Component | Responsibility |
+|-----------|----------------|
+| **Entity** (`Product`) | Plain data object holding state. No database logic. |
+| **Repository** (`ProductRepository`) | Handles all database operations for that entity. |
+
+```
+Traditional MVC Model = Entity + Repository
+```
+
+## Why Use This Approach?
+
+1. **Single Responsibility** - Each class has one job. Entities hold data, repositories handle persistence.
+
+2. **Testability** - Entities can be unit tested without a database. Repositories can be mocked in service tests.
+
+3. **Flexibility** - Swap database implementations without changing entity code. Easy to switch from MySQL to PostgreSQL or add caching.
+
+4. **Clean Domain Logic** - Entities stay focused on business rules, not database concerns.
+
+5. **Reusable Queries** - Common queries live in the repository and can be reused across the application.
+
+## What This Example Shows
+
+- Extending `AbstractRepository` for CRUD operations
+- Implementing `toEntity()` and `toArray()` for mapping
+- Using built-in methods: `findAll()`, `findById()`, `save()`, `deleteById()`
+- Creating custom query methods (`findByCategory()`, `findLowStock()`)
+- Pagination with `paginate()`
+
+## Files
+
+- [`example.php`](example.php) - Main example code
+- [`Product.php`](Product.php) - Product entity class
+- [`ProductRepository.php`](ProductRepository.php) - Repository extending AbstractRepository
+
+## Running the Example
+
+```bash
+php example.php
+```
+
+## Expected Output
+
+The example will demonstrate all repository operations including create, read, update, delete, and pagination.
+
+## Related Examples
+
+- [04-entity-mapping](../04-entity-mapping/) - Entity class basics
+- [12-clean-architecture](../12-clean-architecture/) - Repository with domain separation
+- [13-pagination](../13-pagination/) - Advanced pagination techniques
diff --git a/examples/11-repository-pattern/example.php b/examples/11-repository-pattern/example.php
new file mode 100644
index 0000000..42e3b68
--- /dev/null
+++ b/examples/11-repository-pattern/example.php
@@ -0,0 +1,118 @@
+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],
+ 'category' => [ColOption::TYPE => DataType::VARCHAR, ColOption::SIZE => 50],
+ '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";
+
+ echo "2. Creating Repository:\n";
+ $productRepo = new ProductRepository($database);
+ echo "✓ ProductRepository created\n\n";
+
+ echo "3. Saving Products (Create):\n";
+ $products = [
+ new Product('Laptop', 'Electronics', 999.99, 15),
+ new Product('Mouse', 'Electronics', 29.99, 50),
+ new Product('Keyboard', 'Electronics', 79.99, 5),
+ new Product('Chair', 'Furniture', 199.99, 8),
+ new Product('Desk', 'Furniture', 299.99, 3),
+ new Product('Book', 'Education', 19.99, 100)
+ ];
+
+ foreach ($products as $product) {
+ $productRepo->save($product);
+ echo " ✓ Saved: {$product->name}\n";
+ }
+ echo "\n";
+
+ echo "4. Finding All Products (Read):\n";
+ $allProducts = $productRepo->findAll();
+ echo "Total products: ".count($allProducts)."\n";
+ foreach ($allProducts as $p) {
+ echo " - {$p->name} ({$p->category}): \${$p->price} - Stock: {$p->stock}\n";
+ }
+ echo "\n";
+
+ echo "5. Finding by ID:\n";
+ $product = $productRepo->findById(1);
+ if ($product) {
+ echo " Found: {$product->name} - \${$product->price}\n\n";
+ }
+
+ echo "6. Custom Query - Find by Category:\n";
+ $electronics = $productRepo->findByCategory('Electronics');
+ echo "Electronics products: ".count($electronics)."\n";
+ foreach ($electronics as $p) {
+ echo " - {$p->name}: \${$p->price}\n";
+ }
+ echo "\n";
+
+ echo "7. Custom Query - Find Low Stock:\n";
+ $lowStock = $productRepo->findLowStock(10);
+ echo "Low stock products (< 10): ".count($lowStock)."\n";
+ foreach ($lowStock as $p) {
+ echo " ⚠️ {$p->name}: {$p->stock} remaining\n";
+ }
+ echo "\n";
+
+ echo "8. Updating a Product:\n";
+ $product = $productRepo->findById(3);
+ if ($product) {
+ 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 "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";
+ foreach ($page1->getItems() as $p) {
+ echo " - {$p->name}\n";
+ }
+ echo "\n";
+
+ echo "10. Counting Products:\n";
+ echo " Total products in database: ".$productRepo->count()."\n\n";
+
+ echo "11. Deleting a Product:\n";
+ $productRepo->deleteById(6);
+ echo " ✓ Deleted product with ID 6\n";
+ echo " Products remaining: ".$productRepo->count()."\n\n";
+
+ echo "12. Cleanup:\n";
+ $database->raw("DROP TABLE products")->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) {}
+}
+
+echo "\n=== Example Complete ===\n";
diff --git a/examples/12-clean-architecture/Domain/User.php b/examples/12-clean-architecture/Domain/User.php
new file mode 100644
index 0000000..9644519
--- /dev/null
+++ b/examples/12-clean-architecture/Domain/User.php
@@ -0,0 +1,15 @@
+ $entity->id,
+ 'name' => $entity->name,
+ 'email' => $entity->email,
+ 'age' => $entity->age
+ ];
+ }
+
+ public function findById(mixed $id): ?User {
+ return parent::findById($id);
+ }
+
+ public function findAll(): array {
+ return parent::findAll();
+ }
+
+ public function delete(mixed $id): void {
+ $this->deleteById($id);
+ }
+}
diff --git a/examples/12-clean-architecture/Infrastructure/Repository/UserRepository.php b/examples/12-clean-architecture/Infrastructure/Repository/UserRepository.php
new file mode 100644
index 0000000..4a9ac08
--- /dev/null
+++ b/examples/12-clean-architecture/Infrastructure/Repository/UserRepository.php
@@ -0,0 +1,48 @@
+ $entity->id,
+ 'name' => $entity->name,
+ 'email' => $entity->email,
+ 'age' => $entity->age
+ ];
+ }
+
+ /** @return User[] */
+ public function findByAge(int $minAge): array {
+ $result = $this->getDatabase()->table($this->getTableName())
+ ->select()
+ ->where('age', $minAge, '>=')
+ ->execute();
+
+ return array_map(fn($row) => $this->toEntity($row), $result->fetchAll());
+ }
+}
diff --git a/examples/12-clean-architecture/Infrastructure/Schema/UserTable.php b/examples/12-clean-architecture/Infrastructure/Schema/UserTable.php
new file mode 100644
index 0000000..ea17f7f
--- /dev/null
+++ b/examples/12-clean-architecture/Infrastructure/Schema/UserTable.php
@@ -0,0 +1,18 @@
+getCols()))."\n\n";
+
+ 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";
+
+ echo "3. Using Repository (extends AbstractRepository):\n";
+ $userRepo = new UserRepository($database);
+ echo "✓ UserRepository created\n\n";
+
+ echo "4. Saving Domain Entities:\n";
+ $users = [
+ new User(null, 'Ahmed Ali', 'ahmed@example.com', 28),
+ new User(null, 'Sara Hassan', 'sara@example.com', 35),
+ new User(null, 'Omar Khalil', 'omar@example.com', 22)
+ ];
+
+ foreach ($users as $user) {
+ $userRepo->save($user);
+ echo " ✓ Saved: {$user->name}\n";
+ }
+ echo "\n";
+
+ echo "5. Repository Operations:\n";
+
+ // findAll()
+ $all = $userRepo->findAll();
+ echo "All users (".count($all)."):\n";
+ foreach ($all as $u) {
+ echo " - {$u->name} ({$u->email}) - Age: {$u->age}\n";
+ }
+
+ // findById()
+ $user = $userRepo->findById(1);
+ echo "\nFind by ID 1: {$user->name}\n";
+
+ // Custom method
+ $adults = $userRepo->findByAge(25);
+ echo "\nUsers age >= 25 (".count($adults)."):\n";
+ foreach ($adults as $u) {
+ 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";
+
+ // count()
+ echo "\nTotal count: ".$userRepo->count()."\n\n";
+
+ echo "6. 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) {}
+}
+
+echo "\n=== Example Complete ===\n";
diff --git a/examples/13-pagination/README.md b/examples/13-pagination/README.md
new file mode 100644
index 0000000..b93bf95
--- /dev/null
+++ b/examples/13-pagination/README.md
@@ -0,0 +1,32 @@
+# Pagination
+
+This example demonstrates offset-based and cursor-based pagination techniques.
+
+## What This Example Shows
+
+- Offset-based pagination with `paginate()`
+- Cursor-based pagination with `paginateByCursor()`
+- Working with `Page` and `CursorPage` objects
+- Pagination with ordering
+
+## Files
+
+- [`example.php`](example.php) - Main example code
+- [`User.php`](User.php) - User entity class
+- [`UserRepository.php`](UserRepository.php) - Repository extending AbstractRepository
+
+## Running the Example
+
+```bash
+php example.php
+```
+
+## Expected Output
+
+The example will demonstrate both pagination approaches with sample data, showing page navigation and cursor handling.
+
+## Related Examples
+
+- [11-repository-pattern](../11-repository-pattern/) - Repository with pagination
+- [08-performance-monitoring](../08-performance-monitoring/) - Monitor pagination query performance
+- [02-basic-queries](../02-basic-queries/) - Basic query operations
diff --git a/examples/13-pagination/User.php b/examples/13-pagination/User.php
new file mode 100644
index 0000000..41f8012
--- /dev/null
+++ b/examples/13-pagination/User.php
@@ -0,0 +1,8 @@
+id = (int) $row['id'];
+ $user->name = $row['name'];
+ $user->email = $row['email'];
+ $user->age = (int) $row['age'];
+ return $user;
+ }
+
+ protected function toArray(object $entity): array {
+ return [
+ 'id' => $entity->id,
+ 'name' => $entity->name,
+ 'email' => $entity->email,
+ 'age' => $entity->age
+ ];
+ }
+}
diff --git a/examples/13-pagination/example.php b/examples/13-pagination/example.php
new file mode 100644
index 0000000..919d3a3
--- /dev/null
+++ b/examples/13-pagination/example.php
@@ -0,0 +1,94 @@
+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();
+
+ $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'];
+
+ foreach ($names as $i => $name) {
+ $database->table('users')->insert([
+ 'name' => $name,
+ 'email' => strtolower($name).'@example.com',
+ 'age' => 20 + ($i % 30)
+ ])->execute();
+ }
+ echo "✓ Created 25 test users\n\n";
+
+ $repo = new UserRepository($database);
+
+ 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";
+ foreach ($result->getItems() as $user) {
+ echo " - {$user->name} ({$user->email})\n";
+ }
+ echo " Has next: ".($result->hasNextPage() ? 'Yes' : 'No')."\n\n";
+ }
+
+ echo "3. Cursor-Based Pagination:\n";
+ echo " (Better for large datasets, infinite scroll)\n\n";
+
+ $cursor = null; // null = start from beginning (first page)
+ $pageNum = 1;
+
+ while ($pageNum <= 3) {
+ $result = $repo->paginateByCursor($cursor, 5, 'id', 'ASC');
+ echo "Cursor Page $pageNum:\n";
+ foreach ($result->getItems() as $user) {
+ echo " - ID {$user->id}: {$user->name}\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";
+ $pageNum++;
+ }
+
+ echo "\n4. Pagination with Ordering:\n";
+ $result = $repo->paginate(1, 5, ['age' => 'DESC']);
+ echo "Top 5 oldest users:\n";
+ foreach ($result->getItems() as $user) {
+ echo " - {$user->name} (Age: {$user->age})\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) {}
+}
+
+echo "\n=== Example Complete ===\n";
diff --git a/examples/14-active-record-model/Article.php b/examples/14-active-record-model/Article.php
new file mode 100644
index 0000000..a1d94d9
--- /dev/null
+++ b/examples/14-active-record-model/Article.php
@@ -0,0 +1,66 @@
+db);
+ $article->id = (int) $row['id'];
+ $article->title = $row['title'];
+ $article->content = $row['content'];
+ $article->authorName = $row['author-name'] ?? '';
+ $article->createdAt = $row['created-at'] ?? null;
+ return $article;
+ }
+
+ protected function toArray(object $entity): array {
+ return [
+ 'id' => $entity->id,
+ 'title' => $entity->title,
+ 'content' => $entity->content,
+ 'author-name' => $entity->authorName,
+ ];
+ }
+
+ // Custom query methods
+ public function findByAuthor(string $author): array {
+ $result = $this->getDatabase()->table($this->getTableName())
+ ->select()
+ ->where('author-name', $author)
+ ->execute();
+
+ return array_map(fn($row) => $this->toEntity($row), $result->fetchAll());
+ }
+}
diff --git a/examples/14-active-record-model/README.md b/examples/14-active-record-model/README.md
new file mode 100644
index 0000000..6120b5c
--- /dev/null
+++ b/examples/14-active-record-model/README.md
@@ -0,0 +1,83 @@
+# Active Record Model
+
+This example demonstrates merging Entity and Repository into a single Model class, similar to the Active Record pattern used in many MVC frameworks.
+
+## Concept
+
+Instead of separating Entity and Repository:
+
+```
+Separate (Repository Pattern):
+├── Product.php (Entity - data only)
+└── ProductRepository.php (Repository - database operations)
+
+Merged (Active Record):
+└── Article.php (Model - data + database operations)
+```
+
+## How It Works
+
+The `Article` class:
+1. **Extends `AbstractRepository`** - Inherits all CRUD operations
+2. **Uses `#[Table]` and `#[Column]` attributes** - Defines table structure
+3. **Has public properties** - Holds entity data
+4. **Implements mapping methods** - `toEntity()` and `toArray()`
+
+```php
+#[Table(name: 'articles')]
+class Article extends AbstractRepository {
+ #[Column(type: DataType::INT, primary: true, autoIncrement: true)]
+ public ?int $id = null;
+
+ #[Column(type: DataType::VARCHAR, size: 200)]
+ public string $title = '';
+
+ // ... more properties
+}
+```
+
+## Usage
+
+```php
+// Create and save directly
+$article = new Article($database);
+$article->title = 'My Article';
+$article->content = 'Content here...';
+$article->save(); // Saves itself
+
+// Query using any instance
+$articles = $article->findAll();
+$one = $article->findById(1);
+
+// Update
+$one->title = 'Updated Title';
+$one->save();
+```
+
+## Trade-offs
+
+| Approach | Pros | Cons |
+|----------|------|------|
+| **Merged (Active Record)** | Simple, less files, familiar to MVC developers | Harder to test, mixed responsibilities |
+| **Separate (Repository)** | Testable, flexible, clean separation | More files, more boilerplate |
+
+Choose based on project complexity:
+- **Small projects** → Active Record is simpler
+- **Large projects** → Repository pattern scales better
+
+## Files
+
+- [`example.php`](example.php) - Main example code
+- [`Article.php`](Article.php) - Model class with attributes
+
+## Running the Example
+
+```bash
+php example.php
+```
+
+## Related Examples
+
+- [10-attribute-based-tables](../10-attribute-based-tables/) - Using attributes for table definition
+- [11-repository-pattern](../11-repository-pattern/) - Separate Entity + Repository approach
+- [12-clean-architecture](../12-clean-architecture/) - Full separation with domain layer
diff --git a/examples/14-active-record-model/example.php b/examples/14-active-record-model/example.php
new file mode 100644
index 0000000..96ba72f
--- /dev/null
+++ b/examples/14-active-record-model/example.php
@@ -0,0 +1,99 @@
+addTable($table);
+ $database->table('articles')->createTable()->execute();
+ echo "✓ Articles table created from class attributes\n\n";
+
+ 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
+
+ $article2 = new Article($database);
+ $article2->title = 'Database Patterns';
+ $article2->content = 'Understanding repository pattern...';
+ $article2->authorName = 'Fatima Ali';
+ $article2->save();
+
+ $article3 = new Article($database);
+ $article3->title = 'Advanced PHP';
+ $article3->content = 'PHP 8 features and attributes...';
+ $article3->authorName = 'Ahmad Hassan';
+ $article3->save();
+
+ echo "✓ Articles saved\n\n";
+
+ 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";
+ foreach ($all as $article) {
+ echo " - {$article->title} by {$article->authorName}\n";
+ }
+ echo "\n";
+
+ // Custom query method
+ $byAuthor = $articleModel->findByAuthor('Ahmad Hassan');
+ echo "Articles by Ahmad Hassan:\n";
+ foreach ($byAuthor as $article) {
+ echo " - {$article->title}\n";
+ }
+ echo "\n";
+
+ 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";
+
+ // Delete
+ $first->id = 2;
+ $first->deleteById();
+ echo "✓ Article deleted\n";
+
+ echo "\nRemaining articles:\n";
+ foreach ($articleModel->findAll() as $article) {
+ echo " - [{$article->id}] {$article->title}\n";
+ }
+ echo "\n";
+
+ echo "5. Cleanup:\n";
+ $database->raw("DROP TABLE articles")->execute();
+ echo "✓ Table dropped\n";
+
+} catch (Exception $e) {
+ echo "✗ Error: ".$e->getMessage()."\n";
+ try {
+ $database->raw("DROP TABLE IF EXISTS articles")->execute();
+ } catch (Exception $cleanupError) {}
+}
+
+echo "\n=== Example Complete ===\n";
diff --git a/examples/README.md b/examples/README.md
index 42b74fc..e82bcdc 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -4,15 +4,22 @@ This directory contains practical examples demonstrating how to use the WebFiori
## Examples Overview
-1. **[01-basic-connection](01-basic-connection/)** - How to establish database connections
-2. **[02-basic-queries](02-basic-queries/)** - CRUD operations (Insert, Select, Update, Delete)
-3. **[03-table-blueprints](03-table-blueprints/)** - Creating and managing database table structures
-4. **[04-entity-mapping](04-entity-mapping/)** - Working with entity classes and object mapping
-5. **[05-transactions](05-transactions/)** - Database transactions for data integrity
-6. **[06-migrations](06-migrations/)** - Database schema migrations and versioning
-7. **[07-seeders](07-seeders/)** - Database data seeding and population
-8. **[08-performance-monitoring](08-performance-monitoring/)** - Query performance tracking and analysis
-9. **[09-multi-result-queries](09-multi-result-queries/)** - Multi-result query handling and stored procedures
+| # | Example | Description |
+|---|---------|-------------|
+| 01 | [basic-connection](01-basic-connection/) | Establishing database connections |
+| 02 | [basic-queries](02-basic-queries/) | CRUD operations (Insert, Select, Update, Delete) |
+| 03 | [table-blueprints](03-table-blueprints/) | Creating and managing database table structures |
+| 04 | [entity-mapping](04-entity-mapping/) | Working with entity classes and object mapping |
+| 05 | [transactions](05-transactions/) | Database transactions for data integrity |
+| 06 | [migrations](06-migrations/) | Database schema migrations and versioning |
+| 07 | [seeders](07-seeders/) | Database data seeding and population |
+| 08 | [performance-monitoring](08-performance-monitoring/) | Query performance tracking and analysis |
+| 09 | [multi-result-queries](09-multi-result-queries/) | Multi-result query handling and stored procedures |
+| 10 | [attribute-based-tables](10-attribute-based-tables/) | PHP 8 attributes for table definitions |
+| 11 | [repository-pattern](11-repository-pattern/) | Repository pattern with AbstractRepository |
+| 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 |
## Prerequisites
@@ -47,64 +54,82 @@ You can modify the connection parameters in each example as needed.
- Testing connections with simple queries
### 02-basic-queries
-- Table creation and management
-- INSERT operations with data validation
-- SELECT operations with filtering and conditions
-- UPDATE operations with WHERE clauses
-- DELETE operations with conditions
-- Query result handling
+- Using `raw()` method for SQL queries with parameters
+- INSERT, SELECT, UPDATE, DELETE operations
+- Multi-result queries with stored procedures
### 03-table-blueprints
-- Creating table blueprints with column definitions
-- Using different data types (INT, VARCHAR, TEXT, TIMESTAMP)
+- Creating table blueprints with `createBlueprint()`
+- Using `ColOption` and `DataType` constants
- Setting column constraints (PRIMARY KEY, NOT NULL, AUTO_INCREMENT)
-- Creating foreign key relationships
+- Creating foreign key relationships with `addReference()`
- Generating and executing CREATE TABLE statements
### 04-entity-mapping
-- Generating entity classes from table blueprints
+- Generating entity classes from table blueprints using `EntityMapper`
+- Auto-generated getters/setters and `map()` method
- Mapping database records to PHP objects
-- Working with mapped objects and their methods
-- Filtering and manipulating object collections
### 05-transactions
-- Creating database transactions for data integrity
-- Handling successful transaction commits
-- Automatic rollback on transaction failures
-- Error handling within transactions
+- Creating database transactions with `transaction()` method
+- Automatic commit on success
+- Automatic rollback on exception
- Complex multi-table operations
### 06-migrations
- Creating migration classes extending `AbstractMigration`
+- Implementing `up()` and `down()` methods
- Using `SchemaRunner` for migration management
-- Registering migrations with the schema runner
-- Applying and rolling back migrations
-- Schema change tracking and versioning
+- Applying migrations with `apply()`
+- Rolling back with `rollbackUpTo()`
+- Schema change tracking
### 07-seeders
- Creating seeder classes extending `AbstractSeeder`
+- Implementing `run()` method
+- Environment-specific seeding with `getEnvironments()`
- Using `SchemaRunner` for seeder management
-- Registering seeders with the schema runner
-- Populating database with sample data
-- Environment-specific seeding
### 08-performance-monitoring
-- Configuring performance monitoring settings
-- Tracking query execution times and statistics
-- Identifying slow queries and performance bottlenecks
-- Using `PerformanceAnalyzer` for detailed analysis
-- Performance optimization recommendations
+- Configuring performance monitoring with `setPerformanceConfig()`
+- Using `PerformanceOption` constants
+- Tracking query execution times
+- Using `PerformanceAnalyzer` for metrics
+- Identifying slow queries
### 09-multi-result-queries
-- Executing stored procedures that return multiple result sets
+- Executing stored procedures returning multiple result sets
- Working with `MultiResultSet` objects
-- Processing individual result sets from multi-result queries
-- Parameterized stored procedure calls using `raw()` method
-- Complex business reporting with multiple data views
-
-## Notes
-
-- All examples include proper error handling and cleanup
-- Generated files (like entity classes) are automatically cleaned up
-- Examples use temporary tables that are dropped after execution
-- Each example is thoroughly tested and produces expected output
+- Processing individual result sets
+
+### 10-attribute-based-tables
+- Using PHP 8 attributes: `#[Table]`, `#[Column]`, `#[ForeignKey]`
+- Building tables with `AttributeTableBuilder::build()`
+- Defining entities with attribute-based schema
+
+### 11-repository-pattern
+- Extending `AbstractRepository` for CRUD operations
+- Implementing `toEntity()` and `toArray()` methods
+- Using built-in methods: `findAll()`, `findById()`, `save()`, `deleteById()`
+- Creating custom query methods
+- Pagination with `paginate()`
+
+### 13-pagination
+- Offset-based pagination with `paginate()`
+- Cursor-based pagination with `paginateByCursor()`
+- Working with `Page` and `CursorPage` objects
+- Pagination with ordering
+
+
+### 12-clean-architecture
+- Separating Domain from Infrastructure
+- Pure domain entities (no framework dependencies)
+- Repository interface in Domain layer
+- Database implementation in Infrastructure layer
+- Dependency inversion principle
+
+### 14-active-record-model
+- Merging Entity and Repository into a single Model class
+- Using attributes to define table structure on the model
+- Active Record pattern for simpler projects
+- Trade-offs vs Repository pattern
diff --git a/tests/WebFiori/Tests/Database/Attributes/ForeignKeyAttributeTest.php b/tests/WebFiori/Tests/Database/Attributes/ForeignKeyAttributeTest.php
new file mode 100644
index 0000000..f24d2bc
--- /dev/null
+++ b/tests/WebFiori/Tests/Database/Attributes/ForeignKeyAttributeTest.php
@@ -0,0 +1,117 @@
+ 'id'], name: 'fk_item_order', onUpdate: 'cascade', onDelete: 'cascade')]
+#[ForeignKey(table: 'products', columns: ['product-id' => 'id'], name: 'fk_item_product', onUpdate: 'cascade', onDelete: 'cascade')]
+class TestOrderItem {
+}
+
+#[Table(name: 'composite_ref')]
+#[Column(name: 'tenant-id', type: DataType::INT)]
+#[Column(name: 'user-id', type: DataType::INT)]
+#[Column(name: 'data', type: DataType::VARCHAR, size: 100)]
+#[ForeignKey(table: 'tenant_users', columns: ['tenant-id' => 'tenant_id', 'user-id' => 'user_id'], name: 'fk_composite', onUpdate: 'cascade', onDelete: 'cascade')]
+class TestCompositeFK {
+}
+
+class ForeignKeyAttributeTest extends TestCase {
+ public function testSingleColumnFK() {
+ $fk = new ForeignKey(table: 'users', column: 'id');
+ $this->assertEquals(['id'], $fk->getColumnsMap());
+ }
+
+ public function testMultipleColumnsFK() {
+ $fk = new ForeignKey(table: 'users', columns: ['local_id' => 'id', 'tenant_id' => 'tenant_id']);
+ $this->assertEquals(['local_id' => 'id', 'tenant_id' => 'tenant_id'], $fk->getColumnsMap());
+ }
+
+ public function testBothColumnAndColumnsThrowsException() {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage("ForeignKey: Use either 'column' or 'columns', not both");
+
+ new ForeignKey(table: 'users', column: 'id', columns: ['local_id' => 'id']);
+ }
+
+ public function testClassReferenceResolution() {
+ $table = AttributeTableBuilder::build(TestPost::class, 'mysql');
+ $sql = $table->toSQL();
+
+ $this->assertStringContainsString('`users`', $sql);
+ $this->assertStringContainsString('fk_post_user', $sql);
+ }
+
+ public function testPropertyLevelFK() {
+ $table = AttributeTableBuilder::build(TestPost::class, 'mysql');
+ $sql = $table->toSQL();
+
+ $this->assertStringContainsString('foreign key (`user-id`) references `users` (`id`)', $sql);
+ $this->assertStringContainsString('on update cascade on delete cascade', $sql);
+ }
+
+ public function testClassLevelFKWithColumns() {
+ $table = AttributeTableBuilder::build(TestOrderItem::class, 'mysql');
+ $sql = $table->toSQL();
+
+ $this->assertStringContainsString('fk_item_order', $sql);
+ $this->assertStringContainsString('fk_item_product', $sql);
+ $this->assertStringContainsString('references `orders`', $sql);
+ $this->assertStringContainsString('references `products`', $sql);
+ }
+
+ public function testCompositeForeignKey() {
+ $table = AttributeTableBuilder::build(TestCompositeFK::class, 'mysql');
+ $sql = $table->toSQL();
+
+ $this->assertStringContainsString('fk_composite', $sql);
+ $this->assertStringContainsString('references `tenant_users`', $sql);
+ $this->assertStringContainsString('(`tenant_id`, `user_id`)', $sql);
+ }
+
+ public function testDefaultFKName() {
+ $fk = new ForeignKey(table: 'users', column: 'id');
+ $this->assertNull($fk->name);
+ }
+
+ public function testDefaultOnUpdateOnDelete() {
+ $fk = new ForeignKey(table: 'users', column: 'id');
+ $this->assertEquals('set null', $fk->onUpdate);
+ $this->assertEquals('set null', $fk->onDelete);
+ }
+}
diff --git a/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php b/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php
new file mode 100644
index 0000000..91dd92b
--- /dev/null
+++ b/tests/WebFiori/Tests/Database/Repository/AbstractRepositoryTest.php
@@ -0,0 +1,242 @@
+id = $id;
+ $this->name = $name;
+ $this->value = $value;
+ }
+}
+
+class TestRepository extends AbstractRepository {
+ protected function getTableName(): string {
+ return 'test_entities';
+ }
+
+ protected function getIdField(): string {
+ return 'id';
+ }
+
+ protected function toEntity(array $row): object {
+ return new TestEntity(
+ (int) $row['id'],
+ $row['name'],
+ (int) $row['value']
+ );
+ }
+
+ protected function toArray(object $entity): array {
+ return [
+ 'id' => $entity->id,
+ 'name' => $entity->name,
+ 'value' => $entity->value
+ ];
+ }
+}
+
+class AbstractRepositoryTest extends TestCase {
+ private static ?Database $db = null;
+ private static ?TestRepository $repo = 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->createBlueprint('test_entities')->addColumns([
+ 'id' => [
+ ColOption::TYPE => DataType::INT,
+ ColOption::PRIMARY => true,
+ ColOption::AUTO_INCREMENT => true
+ ],
+ 'name' => [
+ ColOption::TYPE => DataType::VARCHAR,
+ ColOption::SIZE => 100
+ ],
+ 'value' => [
+ ColOption::TYPE => DataType::INT
+ ]
+ ]);
+
+ self::$db->table('test_entities')->createTable()->execute();
+ self::$repo = new TestRepository(self::$db);
+ }
+
+ public static function tearDownAfterClass(): void {
+ self::$db->setQuery('DROP TABLE IF EXISTS test_entities');
+ self::$db->execute();
+ }
+
+ protected function setUp(): void {
+ self::$db->table('test_entities')->delete()->execute();
+ }
+
+ public function testSaveNewEntity() {
+ $entity = new TestEntity(null, 'Test', 100);
+ self::$repo->save($entity);
+
+ $this->assertEquals(1, self::$repo->count());
+ $found = self::$repo->findAll()[0];
+ $this->assertEquals('Test', $found->name);
+ $this->assertEquals(100, $found->value);
+ }
+
+ public function testSaveExistingEntity() {
+ $entity = new TestEntity(null, 'Original', 50);
+ self::$repo->save($entity);
+
+ $found = self::$repo->findAll()[0];
+ $found->name = 'Updated';
+ $found->value = 75;
+ self::$repo->save($found);
+
+ $this->assertEquals(1, self::$repo->count());
+ $updated = self::$repo->findById($found->id);
+ $this->assertEquals('Updated', $updated->name);
+ $this->assertEquals(75, $updated->value);
+ }
+
+ public function testSaveAllEmpty() {
+ self::$repo->saveAll([]);
+ $this->assertEquals(0, self::$repo->count());
+ }
+
+ public function testSaveAllNewEntities() {
+ $entities = [
+ new TestEntity(null, 'Item1', 10),
+ new TestEntity(null, 'Item2', 20),
+ new TestEntity(null, 'Item3', 30)
+ ];
+
+ self::$repo->saveAll($entities);
+
+ $this->assertEquals(3, self::$repo->count());
+ $all = self::$repo->findAll();
+ $names = array_map(fn($e) => $e->name, $all);
+ $this->assertContains('Item1', $names);
+ $this->assertContains('Item2', $names);
+ $this->assertContains('Item3', $names);
+ }
+
+ public function testSaveAllExistingEntities() {
+ // Insert initial entities
+ self::$repo->saveAll([
+ new TestEntity(null, 'A', 1),
+ new TestEntity(null, 'B', 2)
+ ]);
+
+ // Update them
+ $all = self::$repo->findAll();
+ foreach ($all as $entity) {
+ $entity->value = $entity->value * 10;
+ }
+ self::$repo->saveAll($all);
+
+ $this->assertEquals(2, self::$repo->count());
+ $updated = self::$repo->findAll();
+ $values = array_map(fn($e) => $e->value, $updated);
+ sort($values);
+ $this->assertEquals([10, 20], $values);
+ }
+
+ public function testSaveAllMixed() {
+ // Insert one entity first
+ self::$repo->save(new TestEntity(null, 'Existing', 100));
+ $existing = self::$repo->findAll()[0];
+ $existing->name = 'Modified';
+
+ // Save mix of new and existing
+ self::$repo->saveAll([
+ $existing,
+ new TestEntity(null, 'New1', 200),
+ new TestEntity(null, 'New2', 300)
+ ]);
+
+ $this->assertEquals(3, self::$repo->count());
+
+ $modified = self::$repo->findById($existing->id);
+ $this->assertEquals('Modified', $modified->name);
+
+ $all = self::$repo->findAll();
+ $names = array_map(fn($e) => $e->name, $all);
+ $this->assertContains('New1', $names);
+ $this->assertContains('New2', $names);
+ }
+
+ public function testFindByIdWithNullThrowsException() {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot find: no ID provided');
+
+ self::$repo->findById(null);
+ }
+
+ public function testDeleteByIdWithNullThrowsException() {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot delete: no ID provided');
+
+ self::$repo->deleteById(null);
+ }
+
+ public function testFindByIdWithValidId() {
+ self::$repo->save(new TestEntity(null, 'FindMe', 42));
+ $all = self::$repo->findAll();
+ $id = $all[0]->id;
+
+ $found = self::$repo->findById($id);
+
+ $this->assertNotNull($found);
+ $this->assertEquals('FindMe', $found->name);
+ }
+
+ public function testDeleteByIdWithValidId() {
+ self::$repo->save(new TestEntity(null, 'DeleteMe', 99));
+ $all = self::$repo->findAll();
+ $id = $all[0]->id;
+
+ self::$repo->deleteById($id);
+
+ $this->assertEquals(0, self::$repo->count());
+ }
+
+ public function testSaveWithNullOnPureRepoThrowsException() {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot save: no entity provided');
+
+ self::$repo->save();
+ }
+
+ public function testReloadWithEntity() {
+ $entity = new TestEntity(null, 'Original', 100);
+ self::$repo->save($entity);
+ $saved = self::$repo->findAll()[0];
+
+ // Modify in database directly
+ self::$db->table('test_entities')
+ ->update(['name' => 'Modified'])
+ ->where('id', $saved->id)
+ ->execute();
+
+ $reloaded = self::$repo->reload($saved);
+
+ $this->assertEquals('Modified', $reloaded->name);
+ }
+
+ public function testReloadWithNullOnPureRepoThrowsException() {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot find: no ID provided');
+
+ self::$repo->reload();
+ }
+}