We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Extend Criteria?

I would love to be able to extend the Criteria class and do something like this:

User::query
    ->where('id > 100')
    ->myCustomMethod()
    ->orderBy('username')
    ->execute();

Is this currently possible? Any way at all to do this?

So a little ghetto, but if you override Model::query() you can hijack the the criteria class with your you own implementation:

    public static function query(\Phalcon\DiInterface $dependencyInjector = null)
    {
        /**
         * Use the global dependency injector if there is no one defined
         */
        if (!is_object($dependencyInjector)) {
            $dependencyInjector = \Phalcon\Di::getDefault();
        }

        $criteria = new \App\Somewhere\Criteria();
        $criteria->setDI($dependencyInjector);
        $criteria->setModelName(get_called_class());

        return $criteria;
    }

\App\Somewhere\Criteria

namespace App\Somewhere\Model;

use Phalcon\Mvc\Model\Criteria as PhalconCriteria;

class Criteria extends PhalconCriteria
{
    public function myCustomMethod($param)
    {      
        return $this->where("crazy long query you dont want to write every where");
    }
}

I don't undesrtand why in the Model class they hardcode the Criteria namespace:

    public static function query(<DiInterface> dependencyInjector = null) -> <Criteria>
    {
        var criteria;

        /**
         * Use the global dependency injector if there is no one defined
         */
        if typeof dependencyInjector != "object" {
            let dependencyInjector = Di::getDefault();
        }

        /**
         * Gets Criteria instance from DI container
         */
        if dependencyInjector instanceof DiInterface {
            // **HERE** WHY IS THIS HARDCODED?
            let criteria = <CriteriaInterface> dependencyInjector->get("Phalcon\\Mvc\\Model\\Criteria");
        } else {
            let criteria = new Criteria();
            criteria->setDI(dependencyInjector);
        }

        criteria->setModelName(get_called_class());

        return criteria;
}

https://github.com/phalcon/cphalcon/blob/master/phalcon/mvc/model.zep#L974

Would make more sense to check for a custom implementation before falling back to the Phalcon implementation.

edited Jun '17

I'd reverse the approach, and create a "criteria manager" class:

class CriteriaManager {
    protected $_criteria;
    public function attach(CriteriaInterface $criteria) {
        $this->_criteria = $criteria;
    }

    public function myCustomMethod() {
        $this->_criteria->andWhere("1=1 AND column='value'");
    }

    public function execute() {
        return $this->_criteria->execute();
    }
}

Maybe not with this exact layout, but I'm sure you get the concept ;]

The real pro about this approach is that you don't have to hack into existing classes.

edited Jun '17

You can also use Phalcon\Db\Dialect\MySQL to create custom function.
Example :

use Phalcon\Db\Dialect\MySQL as SqlDialect;
use Manager\User as UserManager;
$di->set('db', function () use ($config) {  
    $dialect = new SqlDialect();
    $dialect->registerCustomFunction('GET_YEARDIFF', function($dialect, $expression) {
            return sprintf(
                'TIMESTAMPDIFF(YEAR, %s, %s)',
                $dialect->getSqlExpression($expression['arguments'][0]),
                $dialect->getSqlExpression($expression['arguments'][1])
             );
        }
    );
    $dialect->registerCustomFunction('GROUP_AGE', function($dialect, $expression) {
            return str_replace('[age]', $dialect->getSqlExpression($expression['arguments'][0]),
                'CASE 
                    WHEN [age] <= 17 THEN \''.UserManager::$ageGroup[0]['name'].'\'
                    WHEN [age] <= 25 THEN \''.UserManager::$ageGroup[1]['name'].'\'
                    WHEN [age] <= 35 THEN \''.UserManager::$ageGroup[2]['name'].'\'
                    WHEN [age] <= 50 THEN \''.UserManager::$ageGroup[3]['name'].'\'
                    WHEN [age] > 50 THEN \''.UserManager::$ageGroup[4]['name'].'\'
                    ELSE \''.UserManager::$ageGroup[5]['name'].'\'
                END'           
            );
        }
    );

    return new Mysql([
        'host' => $config[ENV]->database->host,
        'username' => $config[ENV]->database->username,
        'password' => $config[ENV]->database->password,
        'dbname' => $config[ENV]->database->dbname,
        'charset' => $config[ENV]->database->charset,
        'dialectClass' => $dialect
    ]);
});

User::find(['columns'=>'*, GROUP_AGE(GET_YEARDIFF(us_birthday_date, current_date())) as groupAgeName']);

Thats probably a better idea. How would you then hook this into your model? I still want to be able to call Model::query() so that I don't break the existing api.

I'd reverse the approach, and create a "criteria manager" class:

class CriteriaManager {
   protected $_criteria;
  public function attach(CriteriaInterface $criteria) {
      $this->_criteria = $criteria;
  }

  public function myCustomMethod() {
      $this->_criteria->andWhere("1=1 AND column='value'");
  }

  public function execute() {
      return $this->_criteria->execute();
  }
}

Maybe not with this exact layout, but I'm sure you get the concept ;]

The real pro about this approach is that you don't have to hack into existing classes.

edited Jun '17

Yes, dialects are also part of what I want to do. Wouldnt it be great to do this?

$ageGroups = User::query()->groupByAge()->execute();

Code doesnt come much cleaner than that.

You can also use Phalcon\Db\Dialect\MySQL to create custom function.
Example :

use Phalcon\Db\Dialect\MySQL as SqlDialect;
use Manager\User as UserManager;
$di->set('db', function () use ($config) {  
   $dialect = new SqlDialect();
   $dialect->registerCustomFunction('GET_YEARDIFF', function($dialect, $expression) {
           return sprintf(
               'TIMESTAMPDIFF(YEAR, %s, %s)',
               $dialect->getSqlExpression($expression['arguments'][0]),
               $dialect->getSqlExpression($expression['arguments'][1])
            );
       }
   );
   $dialect->registerCustomFunction('GROUP_AGE', function($dialect, $expression) {
           return str_replace('[age]', $dialect->getSqlExpression($expression['arguments'][0]),
               'CASE 
                   WHEN [age] <= 17 THEN \''.UserManager::$ageGroup[0]['name'].'\'
                   WHEN [age] <= 25 THEN \''.UserManager::$ageGroup[1]['name'].'\'
                   WHEN [age] <= 35 THEN \''.UserManager::$ageGroup[2]['name'].'\'
                   WHEN [age] <= 50 THEN \''.UserManager::$ageGroup[3]['name'].'\'
                   WHEN [age] > 50 THEN \''.UserManager::$ageGroup[4]['name'].'\'
                   ELSE \''.UserManager::$ageGroup[5]['name'].'\'
               END'           
           );
       }
   );

   return new Mysql([
       'host' => $config[ENV]->database->host,
       'username' => $config[ENV]->database->username,
       'password' => $config[ENV]->database->password,
       'dbname' => $config[ENV]->database->dbname,
       'charset' => $config[ENV]->database->charset,
       'dialectClass' => $dialect
   ]);
});

User::find(['columns'=>'*, GROUP_AGE(GET_YEARDIFF(us_birthday_date, current_date())) as groupAgeName']);