Skip to content

make core classes chainable #1661

@fiammybe

Description

@fiammybe

Chainable classes add a lot to the Developer experience, as they give a good visual indication of what happens without needing too much text. The best of all : they can be implemented with backward-compatibility in mind.

Based on the ImpressCMS codebase structure, here are the specific modifications needed to implement fluent interfaces:

1. Specific Class Categories for Method Chaining

High Priority Categories:

  • Database Criteria Builders (icms/db/criteria/)
  • IPF Handlers (icms/ipf/Handler.php)
  • Form Builders (icms/form/)
  • Configuration Classes (icms/config/)

Medium Priority:

  • Event System (icms/Event/)
  • Cache Classes (icms/cache/)
  • Template System (icms/template/)

2. Concrete Code Examples

Database Criteria System

Current Implementation (breaks chaining):

class icms_db_criteria_Compo extends icms_db_criteria_Element {
    public function add($element, $condition = 'AND') {
        $this->criteriaElements[] = $element;
        $this->conditions[] = $condition;
        // Returns nothing - breaks chaining
    }
}

Refactored Version (enables chaining):

public function add($element, $condition = 'AND'): self {
    $this->criteriaElements[] = $element;
    $this->conditions[] = $condition;
    return $this;
}

public function addItem(string $column, $value = '', string $operator = '='): self {
    return $this->add(new icms_db_criteria_Item($column, $value, $operator));
}

public function where(string $column, $value, string $operator = '='): self {
    return $this->addItem($column, $value, $operator);
}

public function orWhere(string $column, $value, string $operator = '='): self {
    return $this->add(new icms_db_criteria_Item($column, $value, $operator), 'OR');
}

Usage Example:

$criteria = (new icms_db_criteria_Compo())
    ->where('status', 1)
    ->where('type', 'news')
    ->orWhere('featured', 1)
    ->setLimit(10)
    ->setSort('created_date', 'DESC');

IPF Handler Modifications

Current Implementation:

public function updateAll($fieldname, $fieldvalue, $criteria = null, $force = false) {
    // ... implementation ...
    return $result; // Returns boolean - breaks chaining
}

Refactored Version:

public function updateAll($fieldname, $fieldvalue, $criteria = null, $force = false): bool {
    // ... existing implementation ...
    return $result;
}

public function setField(string $fieldname, $fieldvalue): self {
    $this->pendingUpdates[$fieldname] = $fieldvalue;
    return $this;
}

public function whereField(string $column, $value, string $operator = '='): self {
    if (!$this->updateCriteria) {
        $this->updateCriteria = new icms_db_criteria_Compo();
    }
    $this->updateCriteria->add(new icms_db_criteria_Item($column, $value, $operator));
    return $this;
}

public function execute(): bool {
    foreach ($this->pendingUpdates as $field => $value) {
        $result = $this->updateAll($field, $value, $this->updateCriteria);
        if (!$result) return false;
    }
    $this->pendingUpdates = [];
    $this->updateCriteria = null;
    return true;
}

3. Technical Rationale

Why Current Implementation Prevents Chaining:

  • Methods return void or primitive types (bool, int)
  • No consistent return of $this or class instance
  • Methods perform immediate actions rather than building state

PHP Language Features Enabling Chaining:

  • Return Type Declarations: public function method(): self
  • Self Return: return $this; enables continued chaining
  • Method Visibility: Public methods can be chained externally

Backward Compatibility Strategy:

abstract class icms_db_criteria_Element {
    // Add fluent methods while keeping existing ones
    public function setSort(string $sort, string $order = 'ASC'): self {
        $this->sort = $sort;
        $this->order = $order;
        return $this;
    }
    
    public function setLimit(int $limit, int $start = 0): self {
        $this->limit = $limit;
        $this->start = $start;
        return $this;
    }
    
    public function setGroupBy(string $groupby): self {
        $this->groupby = $groupby;
        return $this;
    }
    
    // Keep existing methods for BC
    public function getSort(): string { return $this->sort; }
    public function getOrder(): string { return $this->order; }
    public function getLimit(): int { return $this->limit; }
}

4. Implementation Priority

Phase 1 (Immediate Impact):

  1. icms_db_criteria_Compo - Most used for database queries
  2. icms_db_criteria_Element - Base class affects all criteria
  3. icms_db_criteria_Item - Individual conditions

Phase 2 (Handler Integration):

  1. icms_ipf_Handler - Core handler functionality
  2. Form builders in icms/form/ directory
  3. Configuration classes

Phase 3 (Advanced Features):

  1. Event system method chaining
  2. Template assignment chaining
  3. Cache operation chaining

5. Potential Breaking Changes & Mitigation

Breaking Changes:

  • Methods that previously returned void now return self
  • Some method signatures change (adding return types)

Mitigation Strategies:

class icms_db_criteria_Compo extends icms_db_criteria_Element {
    // Maintain BC with original method
    public function add($element, $condition = 'AND') {
        $this->criteriaElements[] = $element;
        $this->conditions[] = $condition;
        return $this; // Now returns self instead of void
    }
    
    // Add alias for clarity
    public function addCriteria($element, $condition = 'AND'): self {
        return $this->add($element, $condition);
    }
}

Testing Strategy:

  1. Unit tests for each modified method
  2. Integration tests for chained operations
  3. Backward compatibility tests ensuring existing code works

Example Complete Implementation:

<?php
class icms_db_criteria_QueryBuilder extends icms_db_criteria_Compo {
    
    public function select(array $fields = ['*']): self {
        $this->selectFields = $fields;
        return $this;
    }
    
    public function from(string $table): self {
        $this->table = $table;
        return $this;
    }
    
    public function where(string $column, $value, string $operator = '='): self {
        return $this->add(new icms_db_criteria_Item($column, $value, $operator));
    }
    
    public function orWhere(string $column, $value, string $operator = '='): self {
        return $this->add(new icms_db_criteria_Item($column, $value, $operator), 'OR');
    }
    
    public function orderBy(string $column, string $direction = 'ASC'): self {
        return $this->setSort($column, $direction);
    }
    
    public function limit(int $limit, int $offset = 0): self {
        return $this->setLimit($limit, $offset);
    }
    
    public function toSql(): string {
        $sql = 'SELECT ' . implode(', ', $this->selectFields);
        $sql .= ' FROM ' . $this->table;
        $sql .= $this->renderWhere();
        
        if ($this->getSort()) {
            $sql .= ' ORDER BY ' . $this->getSort() . ' ' . $this->getOrder();
        }
        
        if ($this->getLimit()) {
            $sql .= ' LIMIT ' . $this->getLimit();
            if ($this->getStart()) {
                $sql .= ' OFFSET ' . $this->getStart();
            }
        }
        
        return $sql;
    }
}

This approach provides immediate benefits while maintaining backward compatibility and follows ImpressCMS coding standards from CLAUDE.md.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions