Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a7fc0ce
chore: Updated License Headers
usernane Dec 30, 2025
2803828
chore: Updated License Headers
usernane Dec 30, 2025
ff78798
feat: Repo
usernane Dec 31, 2025
9ea1fcf
Update ResultSet.php
usernane Dec 31, 2025
83c4a5c
feat: Attributes
usernane Dec 31, 2025
dbde80c
feat: Entity Generator
usernane Jan 1, 2026
a4f1a7f
refactor: Move Classes to Correct NS
usernane Jan 1, 2026
889a65d
refactor: Namespaces Correction
usernane Jan 1, 2026
457c8ef
refactor: Enhanced Migrations Runner
usernane Jan 1, 2026
aaae1f0
feat: Migration/Seeder Discovery
usernane Jan 1, 2026
23c2a9b
fix: Ignore if Migration Already Registered
usernane Jan 1, 2026
e5694e6
feat: Batching of Migrations
usernane Jan 1, 2026
699673a
feat: Add Support for Dry Run
usernane Jan 4, 2026
a5d4ef6
feat: Add Support for `DatabaseChangeResult`
usernane Jan 4, 2026
4ebe09d
feat: Add Support for `DatabaseChangeGenerator`
usernane Jan 4, 2026
302f3ef
refactor: Removal of `setDatabase/getDatabase`
usernane Jan 4, 2026
dca1048
feat: Add Support for Getting Connection Info Under Change
usernane Jan 4, 2026
145ad9c
feat: Wrap Transitions in Changes
usernane Jan 4, 2026
9ab0c9e
refactor: Fix Missing Imports
usernane Jan 4, 2026
815588f
Create MSSQLAttributeTestUser.php
usernane Jan 4, 2026
e66aff0
Create MSSQLAttributeTestPost.php
usernane Jan 4, 2026
bf09ed1
fix: Imports Correction
usernane Jan 4, 2026
da6a6e6
refactor: Mixed Data Type
usernane Jan 4, 2026
e56a1ba
fix(mysql): Auto-Increment
usernane Jan 4, 2026
180ebec
test: Fix Test Case
usernane Jan 4, 2026
caddba0
test: Fix Test Case
usernane Jan 4, 2026
95cdf0a
Update SchemaErrorHandlingTest.php
usernane Jan 4, 2026
82d3af7
Update SchemaRunnerTest.php
usernane Jan 4, 2026
f7055c9
Update SchemaRunnerTest.php
usernane Jan 4, 2026
adab255
Update SchemaChangeRepository.php
usernane Jan 4, 2026
a43b66e
Update DryRunTest.php
usernane Jan 4, 2026
2c1ff4f
Update SchemaErrorHandlingTest.php
usernane Jan 4, 2026
74d3ffd
chore: Run CS Fixer
usernane Jan 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion WebFiori/Database/AbstractQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2019 Ibrahim BinAlshikh
* Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
Expand All @@ -14,6 +14,9 @@
use Throwable;
use WebFiori\Database\MsSql\MSSQLQuery;
use WebFiori\Database\MySql\MySQLQuery;
use WebFiori\Database\Query\Condition;
use WebFiori\Database\Query\Expression;
use WebFiori\Database\Query\InsertBuilder;
/**
* A base class that can be used to build SQL queries.
*
Expand Down
151 changes: 151 additions & 0 deletions WebFiori/Database/Attributes/AttributeTableBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
namespace WebFiori\Database\Attributes;

use ReflectionClass;
use WebFiori\Database\ColOption;
use WebFiori\Database\DataType;
use WebFiori\Database\MsSql\MSSQLTable;
use WebFiori\Database\MySql\MySQLTable;
use WebFiori\Database\Table as TableClass;

class AttributeTableBuilder {
public static function build(string $entityClass, string $dbType = 'mysql'): TableClass {
$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
];
}

// Check for class-level ForeignKey attributes
$classFkAttrs = $reflection->getAttributes(ForeignKey::class);

foreach ($classFkAttrs as $fkAttr) {
$fkConfig = $fkAttr->newInstance();
$foreignKeys[] = [
'property' => $fkConfig->column,
'config' => $fkConfig
];
}
} 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] = [
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
];
}
}
}

$table->addColumns($columns);

// Store table references for foreign keys
$tableRegistry = [];

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;
}

$table->addReference(
$tableRegistry[$refTableName],
[$fk['property'] => $refColName],
$fk['config']->name ?? 'fk_'.$fk['property'],
$fk['config']->onUpdate,
$fk['config']->onDelete
);
}

return $table;
}

private static function propertyToKey(string $propertyName): string {
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $propertyName));
}
}
24 changes: 24 additions & 0 deletions WebFiori/Database/Attributes/Column.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
namespace WebFiori\Database\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Column {
public function __construct(
public string $type,
public ?int $size = null,
public ?int $scale = null,
public bool $primary = false,
public bool $unique = false,
public bool $nullable = false,
public bool $autoIncrement = false,
public bool $identity = false,
public bool $autoUpdate = false,
public mixed $default = null,
public ?string $name = null,
public ?string $comment = null,
public mixed $callback = null
) {
}
}
16 changes: 16 additions & 0 deletions WebFiori/Database/Attributes/ForeignKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
namespace WebFiori\Database\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ForeignKey {
public function __construct(
public string $table,
public string $column,
public ?string $name = null,
public string $onUpdate = 'set null',
public string $onDelete = 'set null'
) {
}
}
13 changes: 13 additions & 0 deletions WebFiori/Database/Attributes/Table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
namespace WebFiori\Database\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class Table {
public function __construct(
public string $name,
public ?string $comment = null
) {
}
}
2 changes: 1 addition & 1 deletion WebFiori/Database/ColOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2024 Ibrahim BinAlshikh
* Copyright (c) 2024-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
Expand Down
10 changes: 3 additions & 7 deletions WebFiori/Database/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2019 Ibrahim BinAlshikh
* Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
Expand Down Expand Up @@ -325,15 +325,11 @@ public function getOwner() {
* the column to an entity class. For example, the 'varchar' in MySQL is
* a 'string' in PHP.
*
* @return string A string that represents column datatype in PHP.
* @return string A string that represents column datatype in PHP (int, float, bool, string).
*
*/
public function getPHPType() : string {
if ($this->getDatatype() == 'char') {
return 'string';
}

return 'mixed';
return DataType::toPHPType($this->getDatatype());
}
/**
* Returns the previous table which was owns the column.
Expand Down
2 changes: 1 addition & 1 deletion WebFiori/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2019 Ibrahim BinAlshikh
* Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
Expand Down
2 changes: 1 addition & 1 deletion WebFiori/Database/ConnectionInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2019 Ibrahim BinAlshikh
* Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
Expand Down
17 changes: 16 additions & 1 deletion WebFiori/Database/DataType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2024 Ibrahim BinAlshikh
* Copyright (c) 2024-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
Expand Down Expand Up @@ -204,4 +204,19 @@ class DataType {
* </ul>
*/
const VARCHAR = 'varchar';

/**
* Maps database data type to PHP type.
*
* @param string $dbType The database data type
* @return string The PHP type (int, float, bool, string)
*/
public static function toPHPType(string $dbType): string {
return match (strtolower($dbType)) {
self::INT, self::BIGINT => 'int',
self::FLOAT, self::DOUBLE, self::DECIMAL, self::MONEY => 'float',
self::BOOL, self::BIT => 'bool',
default => 'string'
};
}
}
Loading
Loading