Sprint 1.2 abgeschlossen: Model.php und Collection.php mit vollständigen ORM- und Collection-Features implementiert

This commit is contained in:
thomas 2025-07-06 22:16:25 +02:00
parent a40d17e578
commit 584e831f8c
3 changed files with 856 additions and 12 deletions

View File

@ -4,10 +4,10 @@
**Ziel:** 100% PrestaShop-Kompatibilität mit allen Core- und erweiterten Funktionen
**Timeline:** 6 Monate (24 Wochen) - 6 Milestones mit je 4 Sprints
**Status:** In Bearbeitung
**Aktueller Fortschritt:** 8.33% (2 von 24 Sprints abgeschlossen)
**Aktueller Fortschritt:** 12.5% (3 von 24 Sprints abgeschlossen)
## MILESTONE 1: CORE-SYSTEM ERWEITERUNG (Woche 1-4)
**Status:** In Bearbeitung (50% abgeschlossen)
**Status:** In Bearbeitung (75% abgeschlossen)
### Sprint 1.1: Tools.php & Context.php Erweiterung ✅ ABGESCHLOSSEN
- ✅ Security-Funktionen (hash, getToken, AdminToken, String-Operationen)
@ -17,11 +17,11 @@
- ✅ Context.php Erweiterung (getContext, cloneContext, Device-Erkennung)
- ✅ Cart.php Erweiterung (nbProducts, addCartRule, getOrderTotal, etc.)
### Sprint 1.2: Datenbank & ORM 🔄 IN BEARBEITUNG (75% abgeschlossen)
### Sprint 1.2: Datenbank & ORM ✅ ABGESCHLOSSEN
- ✅ Db.php Erweiterung (query, insert, update, delete, execute, etc.)
- ✅ ObjectModel.php Erweiterung (save, add, update, delete, duplicateObject, validateFields, getFields, formatValue, hydrate, getDefinition, etc.)
- 🔄 Model.php Erweiterung (CRUD-Operationen, Validierung, Beziehungen)
- Collection.php Erweiterung (Filter, Sortierung, Pagination)
- Model.php Erweiterung (CRUD-Operationen, Validierung, Beziehungen)
- Collection.php Erweiterung (Filter, Sortierung, Pagination)
### Sprint 1.3: Core-Klassen System
- ⏳ **Product.php** (8000+ Zeilen) - Vollständige Produktverwaltung
@ -299,8 +299,8 @@
- `getTranslations()`, `updateTranslations()` - Übersetzungsverwaltung
## GESAMTFORTSCHRITT
**Aktueller Stand:** 8.33% (2 von 24 Sprints abgeschlossen)
**Nächster Meilenstein:** Sprint 1.2 abschließen (Model.php & Collection.php)
**Aktueller Stand:** 12.5% (3 von 24 Sprints abgeschlossen)
**Nächster Meilenstein:** Sprint 1.3 starten (Core-Klassen System)
## TECHNISCHE DETAILS
- **Datenbank:** MySQL 8.0+ mit erweiterten Tabellen
@ -317,10 +317,9 @@
- ✅ Dokumentation bei jedem Sprint
## NÄCHSTE SCHRITTE
1. **Sprint 1.2 abschließen:** Model.php und Collection.php implementieren
2. **Sprint 1.3 starten:** Core-Klassen System (Product.php, Category.php, Customer.php, Order.php)
3. **Sprint 1.4 starten:** Erweiterte Core-Klassen (Address.php, Manufacturer.php, Supplier.php, Currency.php, Language.php)
4. **Milestone 1 abschließen:** Core-System vollständig
1. **Sprint 1.3 starten:** Core-Klassen System (Product.php, Category.php, Customer.php, Order.php)
2. **Sprint 1.4 starten:** Erweiterte Core-Klassen (Address.php, Manufacturer.php, Supplier.php, Currency.php, Language.php)
3. **Milestone 1 abschließen:** Core-System vollständig
---
*Letzte Aktualisierung: Sprint 1.2 - ObjectModel.php Erweiterung abgeschlossen*
*Letzte Aktualisierung: Sprint 1.2 (Model.php & Collection.php) abgeschlossen*

225
classes/Collection.php Normal file
View File

@ -0,0 +1,225 @@
<?php
/**
* Collection - Mächtige Sammlung von Model-Objekten mit Filter-, Sortier- und Pagination-Methoden
*/
class Collection implements \IteratorAggregate, \Countable
{
/** @var array */
protected $items = [];
/**
* Konstruktor
* @param array $items
*/
public function __construct(array $items = [])
{
$this->items = $items;
}
/**
* WHERE-Bedingung (Vergleich)
*/
public function where($key, $operator, $value = null)
{
if (func_num_args() == 2) {
$value = $operator;
$operator = '=';
}
$filtered = array_filter($this->items, function ($item) use ($key, $operator, $value) {
$itemValue = is_array($item) ? $item[$key] : $item->$key;
switch ($operator) {
case '=': return $itemValue == $value;
case '!=': return $itemValue != $value;
case '>': return $itemValue > $value;
case '<': return $itemValue < $value;
case '>=': return $itemValue >= $value;
case '<=': return $itemValue <= $value;
case 'like': return stripos($itemValue, $value) !== false;
default: return false;
}
});
return new static(array_values($filtered));
}
/**
* WHERE IN
*/
public function whereIn($key, array $values)
{
$filtered = array_filter($this->items, function ($item) use ($key, $values) {
$itemValue = is_array($item) ? $item[$key] : $item->$key;
return in_array($itemValue, $values);
});
return new static(array_values($filtered));
}
/**
* WHERE BETWEEN
*/
public function whereBetween($key, array $range)
{
$filtered = array_filter($this->items, function ($item) use ($key, $range) {
$itemValue = is_array($item) ? $item[$key] : $item->$key;
return $itemValue >= $range[0] && $itemValue <= $range[1];
});
return new static(array_values($filtered));
}
/**
* WHERE NULL
*/
public function whereNull($key)
{
$filtered = array_filter($this->items, function ($item) use ($key) {
$itemValue = is_array($item) ? $item[$key] : $item->$key;
return is_null($itemValue);
});
return new static(array_values($filtered));
}
/**
* WHERE NOT NULL
*/
public function whereNotNull($key)
{
$filtered = array_filter($this->items, function ($item) use ($key) {
$itemValue = is_array($item) ? $item[$key] : $item->$key;
return !is_null($itemValue);
});
return new static(array_values($filtered));
}
/**
* ORDER BY
*/
public function orderBy($key)
{
$items = $this->items;
usort($items, function ($a, $b) use ($key) {
$aValue = is_array($a) ? $a[$key] : $a->$key;
$bValue = is_array($b) ? $b[$key] : $b->$key;
return $aValue <=> $bValue;
});
return new static($items);
}
/**
* ORDER BY DESC
*/
public function orderByDesc($key)
{
$items = $this->items;
usort($items, function ($a, $b) use ($key) {
$aValue = is_array($a) ? $a[$key] : $a->$key;
$bValue = is_array($b) ? $b[$key] : $b->$key;
return $bValue <=> $aValue;
});
return new static($items);
}
/**
* Neueste zuerst (nach Feld, default: date_add)
*/
public function latest($key = 'date_add')
{
return $this->orderByDesc($key);
}
/**
* Älteste zuerst (nach Feld, default: date_add)
*/
public function oldest($key = 'date_add')
{
return $this->orderBy($key);
}
/**
* Pagination
*/
public function paginate($perPage = 20, $page = 1)
{
$offset = ($page - 1) * $perPage;
$items = array_slice($this->items, $offset, $perPage);
return new static($items);
}
/**
* Einfache Pagination (liefert Array)
*/
public function simplePaginate($perPage = 20, $page = 1)
{
$offset = ($page - 1) * $perPage;
return array_slice($this->items, $offset, $perPage);
}
/**
* Chunk-Verarbeitung
*/
public function chunk($size, callable $callback)
{
$chunks = array_chunk($this->items, $size);
foreach ($chunks as $chunk) {
$callback(new static($chunk));
}
}
/**
* Iterator (each)
*/
public function each(callable $callback)
{
foreach ($this->items as $key => $item) {
$callback($item, $key);
}
return $this;
}
/**
* Zähle Elemente
*/
public function count()
{
return count($this->items);
}
/**
* IteratorAggregate
*/
public function getIterator()
{
return new \ArrayIterator($this->items);
}
/**
* Alle Elemente als Array
*/
public function all()
{
return $this->items;
}
/**
* Erstes Element
*/
public function first()
{
return reset($this->items);
}
/**
* Letztes Element
*/
public function last()
{
return end($this->items);
}
/**
* Leere Collection?
*/
public function isEmpty()
{
return empty($this->items);
}
}

620
classes/Model.php Normal file
View File

@ -0,0 +1,620 @@
<?php
/**
* Model - Base class for all database models
* Extends ObjectModel with additional functionality
*/
abstract class Model extends ObjectModel
{
/** @var array Validation errors */
protected $errors = [];
/** @var array Validation rules */
protected static $rules = [];
/** @var array Relationships */
protected static $relationships = [];
/** @var array Fillable fields */
protected $fillable = [];
/** @var array Hidden fields */
protected $hidden = [];
/** @var array Casts */
protected $casts = [];
/** @var string Primary key */
protected $primaryKey = 'id';
/** @var bool Timestamps */
public $timestamps = true;
/** @var string Created at field */
protected $createdAtField = 'date_add';
/** @var string Updated at field */
protected $updatedAtField = 'date_upd';
/**
* Constructor
*
* @param int|null $id
* @param int|null $id_lang
* @param int|null $id_shop
*/
public function __construct($id = null, $id_lang = null, $id_shop = null)
{
parent::__construct($id, $id_lang, $id_shop);
$this->initializeModel();
}
/**
* Initialize model
*/
protected function initializeModel()
{
// Set default values
if ($this->timestamps) {
if (property_exists($this, $this->createdAtField)) {
$this->{$this->createdAtField} = date('Y-m-d H:i:s');
}
if (property_exists($this, $this->updatedAtField)) {
$this->{$this->updatedAtField} = date('Y-m-d H:i:s');
}
}
}
/**
* Create new model instance
*
* @param array $attributes
* @return static
*/
public static function create(array $attributes = [])
{
$model = new static();
$model->fill($attributes);
$model->save();
return $model;
}
/**
* Find model by ID
*
* @param int $id
* @return static|null
*/
public static function find($id)
{
return new static($id);
}
/**
* Find model by ID or throw exception
*
* @param int $id
* @return static
* @throws Exception
*/
public static function findOrFail($id)
{
$model = static::find($id);
if (!$model || !$model->id) {
throw new Exception('Model not found with ID: ' . $id);
}
return $model;
}
/**
* Find model by field
*
* @param string $field
* @param mixed $value
* @return static|null
*/
public static function findBy($field, $value)
{
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . static::$definition['table'] . '` WHERE `' . pSQL($field) . '` = "' . pSQL($value) . '"';
$result = Db::getInstance()->getRow($sql);
if ($result) {
$model = new static();
$model->hydrate($result);
return $model;
}
return null;
}
/**
* Find model by field or throw exception
*
* @param string $field
* @param mixed $value
* @return static
* @throws Exception
*/
public static function findByOrFail($field, $value)
{
$model = static::findBy($field, $value);
if (!$model) {
throw new Exception('Model not found with ' . $field . ': ' . $value);
}
return $model;
}
/**
* Get all models
*
* @param string $orderBy
* @param string $orderWay
* @return array
*/
public static function all($orderBy = null, $orderWay = 'ASC')
{
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . static::$definition['table'] . '`';
if ($orderBy) {
$sql .= ' ORDER BY `' . pSQL($orderBy) . '` ' . pSQL($orderWay);
}
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new static();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Get models with pagination
*
* @param int $page
* @param int $perPage
* @param string $orderBy
* @param string $orderWay
* @return array
*/
public static function paginate($page = 1, $perPage = 20, $orderBy = null, $orderWay = 'ASC')
{
$offset = ($page - 1) * $perPage;
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . static::$definition['table'] . '`';
if ($orderBy) {
$sql .= ' ORDER BY `' . pSQL($orderBy) . '` ' . pSQL($orderWay);
}
$sql .= ' LIMIT ' . (int) $perPage . ' OFFSET ' . (int) $offset;
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new static();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Count total models
*
* @return int
*/
public static function count()
{
$sql = 'SELECT COUNT(*) as total FROM `' . _DB_PREFIX_ . static::$definition['table'] . '`';
$result = Db::getInstance()->getRow($sql);
return (int) $result['total'];
}
/**
* Check if model exists
*
* @param int $id
* @return bool
*/
public static function exists($id)
{
$sql = 'SELECT COUNT(*) as total FROM `' . _DB_PREFIX_ . static::$definition['table'] . '` WHERE `' . static::$definition['primary'] . '` = ' . (int) $id;
$result = Db::getInstance()->getRow($sql);
return (int) $result['total'] > 0;
}
/**
* Fill model with attributes
*
* @param array $attributes
* @return $this
*/
public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
if (in_array($key, $this->fillable) || empty($this->fillable)) {
$this->{$key} = $value;
}
}
return $this;
}
/**
* Update model
*
* @param array $attributes
* @return bool
*/
public function update(array $attributes = [])
{
$this->fill($attributes);
if ($this->timestamps && property_exists($this, $this->updatedAtField)) {
$this->{$this->updatedAtField} = date('Y-m-d H:i:s');
}
return parent::update();
}
/**
* Delete model
*
* @return bool
*/
public function delete()
{
return parent::delete();
}
/**
* Validate model
*
* @param bool $die
* @param bool $error_return
* @return bool|string
*/
public function validate($die = true, $error_return = false)
{
$this->errors = [];
// Validate required fields
foreach (static::$definition['fields'] as $field => $rules) {
if (isset($rules['required']) && $rules['required']) {
if (!isset($this->{$field}) || empty($this->{$field})) {
$this->errors[] = 'Field ' . $field . ' is required';
}
}
}
// Validate custom rules
if (!empty(static::$rules)) {
foreach (static::$rules as $field => $rules) {
if (isset($this->{$field})) {
$this->validateField($field, $this->{$field}, $rules);
}
}
}
if (!empty($this->errors)) {
if ($die) {
throw new Exception(implode(', ', $this->errors));
}
return $error_return ? implode(', ', $this->errors) : false;
}
return true;
}
/**
* Validate single field
*
* @param string $field
* @param mixed $value
* @param array $rules
*/
protected function validateField($field, $value, $rules)
{
foreach ($rules as $rule) {
switch ($rule) {
case 'email':
if (!Validate::isEmail($value)) {
$this->errors[] = 'Field ' . $field . ' must be a valid email';
}
break;
case 'url':
if (!Validate::isUrl($value)) {
$this->errors[] = 'Field ' . $field . ' must be a valid URL';
}
break;
case 'numeric':
if (!is_numeric($value)) {
$this->errors[] = 'Field ' . $field . ' must be numeric';
}
break;
case 'integer':
if (!is_int($value) && !ctype_digit($value)) {
$this->errors[] = 'Field ' . $field . ' must be an integer';
}
break;
case 'boolean':
if (!is_bool($value) && !in_array($value, [0, 1, '0', '1'])) {
$this->errors[] = 'Field ' . $field . ' must be boolean';
}
break;
}
}
}
/**
* Get validation errors
*
* @return array
*/
public function getValidationErrors()
{
return $this->errors;
}
/**
* Check if model has errors
*
* @return bool
*/
public function hasErrors()
{
return !empty($this->errors);
}
/**
* Get model as array
*
* @return array
*/
public function toArray()
{
$array = [];
foreach (get_object_vars($this) as $key => $value) {
if (!in_array($key, $this->hidden)) {
$array[$key] = $value;
}
}
return $array;
}
/**
* Get model as JSON
*
* @return string
*/
public function toJson()
{
return json_encode($this->toArray());
}
/**
* Define one-to-one relationship
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
* @return mixed
*/
public function hasOne($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$relatedModel = new $related();
return $relatedModel->findBy($foreignKey, $this->{$localKey});
}
/**
* Define one-to-many relationship
*
* @param string $related
* @param string $foreignKey
* @param string $localKey
* @return array
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$relatedModel = new $related();
$sql = 'SELECT * FROM `' . _DB_PREFIX_ . $relatedModel::$definition['table'] . '` WHERE `' . pSQL($foreignKey) . '` = ' . (int) $this->{$localKey};
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new $related();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Define belongs-to relationship
*
* @param string $related
* @param string $foreignKey
* @param string $ownerKey
* @return mixed
*/
public function belongsTo($related, $foreignKey = null, $ownerKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$ownerKey = $ownerKey ?: 'id';
$relatedModel = new $related();
return $relatedModel->find($this->{$foreignKey});
}
/**
* Define many-to-many relationship
*
* @param string $related
* @param string $table
* @param string $foreignPivotKey
* @param string $relatedPivotKey
* @param string $parentKey
* @param string $relatedKey
* @return array
*/
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null)
{
$table = $table ?: $this->joiningTable($related);
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
$relatedPivotKey = $relatedPivotKey ?: $this->getRelatedForeignKey($related);
$parentKey = $parentKey ?: $this->getKeyName();
$relatedKey = $relatedKey ?: 'id';
$relatedModel = new $related();
$sql = 'SELECT r.* FROM `' . _DB_PREFIX_ . $table . '` p
JOIN `' . _DB_PREFIX_ . $relatedModel::$definition['table'] . '` r ON p.`' . pSQL($relatedPivotKey) . '` = r.`' . pSQL($relatedKey) . '`
WHERE p.`' . pSQL($foreignPivotKey) . '` = ' . (int) $this->{$parentKey};
$results = Db::getInstance()->executeS($sql);
$models = [];
foreach ($results as $result) {
$model = new $related();
$model->hydrate($result);
$models[] = $model;
}
return $models;
}
/**
* Get foreign key name
*
* @return string
*/
protected function getForeignKey()
{
return strtolower(class_basename($this)) . '_id';
}
/**
* Get related foreign key name
*
* @param string $related
* @return string
*/
protected function getRelatedForeignKey($related)
{
return strtolower(class_basename($related)) . '_id';
}
/**
* Get joining table name
*
* @param string $related
* @return string
*/
protected function joiningTable($related)
{
$models = [strtolower(class_basename($this)), strtolower(class_basename($related))];
sort($models);
return implode('_', $models);
}
/**
* Get key name
*
* @return string
*/
public function getKeyName()
{
return $this->primaryKey;
}
/**
* Get key value
*
* @return mixed
*/
public function getKey()
{
return $this->{$this->getKeyName()};
}
/**
* Check if model is new
*
* @return bool
*/
public function isNew()
{
return !$this->getKey();
}
/**
* Refresh model from database
*
* @return $this
*/
public function refresh()
{
if ($this->getKey()) {
$fresh = static::find($this->getKey());
if ($fresh) {
$this->hydrate($fresh->toArray());
}
}
return $this;
}
/**
* Touch model (update timestamps)
*
* @return bool
*/
public function touch()
{
if ($this->timestamps && property_exists($this, $this->updatedAtField)) {
$this->{$this->updatedAtField} = date('Y-m-d H:i:s');
return $this->update();
}
return false;
}
/**
* Get table name
*
* @return string
*/
public function getTable()
{
return static::$definition['table'];
}
/**
* Get connection
*
* @return Db
*/
public function getConnection()
{
return Db::getInstance();
}
}