puntogap / yii-conditions

允许以模块化方式重用预构建查询的条件,遵循DRY原则。

安装: 48

依赖: 0

建议: 0

安全性: 0

星标: 1

关注者: 1

分支: 1

开放问题: 0

类型:yii2-extension

dev-master 2021-09-03 17:28 UTC

This package is not auto-updated.

Last update: 2024-09-28 07:49:35 UTC


README

Yii Conditions 是一个扩展ActiveQuery类,允许模块化重用预构建查询的条件,遵循DRY原则。

目录

安装

推荐的安装方法是使用 Composer

composer require puntogap/yii-conditions

使用

准备查询模型

开始使用 Yii Conditions 首先需要的是一个继承自 yii\db\ActiveRecord 类的模型,以及配置为继承自 yii\db\ActiveQuery 类的finder。这个实例将实现本扩展。例如,假设我们有一个用户模型类

namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use app\models\UsuarioQuery;

class Usuario extends ActiveRecord
{
    // ... 

    public static function find()
    {
        return Yii::createObject(UsuarioQuery::className(), [get_called_class()]);
    }

    // ... 
}
namespace app\models;

use Yii;
use yii\db\ActiveQuery;

class UsuarioQuery extends ActiveQuery
{
    // ... 
}

然后将此特质添加到查询类中

namespace app\models;

use Yii;
use yii\db\ActiveQuery;
use PuntoGAP\YiiConditions\Conditions;

class UsuarioQuery extends ActiveQuery
{
    use Conditions;

    // ... 
}

添加此功能后,该类已准备好使用 Yii Conditions

创建第一个条件。

假设在原始查询类中有一个用于查询(或修改查询)关于活跃用户的方法。

namespace app\models;

use Yii;
use yii\db\ActiveQuery;

class UsuarioQuery extends ActiveQuery
{
    // ... 

    public function activos()
    {
        return $this->andWhere(['activo' => 1]);
    }
    
    // ... 
}

如果我们按照以下方式定义方法

namespace app\models;

use Yii;
use yii\db\ActiveQuery;
use PuntoGAP\YiiConditions\Conditions;

class UsuarioQuery extends ActiveQuery
{
    use Conditions;
    
    // ... 

    protected function conditionActivos()
    {
        return ['activo' => 1];
    }
    
    // ... 
}

Conditions特质的行为是,当调用 app\models\Usuario::find()->activos()->all() 时,这个最后的实现结果将与前面的实现结果相同。然而,这种第二种实现现在提供了一系列重大优势,这是第一种实现无法提供的,如下所述。

Yii Conditions 实现了一个 流畅的接口,因此可以链式添加必要的条件,并将其组合在一起。

逻辑修饰符

从方法调用中修饰逻辑

定义任意条件 conditionFoo() 后,可以调用以下方法

Model::find()->foo()->all();  // and where [condition]
Model::find()->andFoo()->all(); // and where [condition] , equivalente al llamado anterior
Model::find()->orFoo()->all(); // or where [condition]
Model::find()->notFoo()->all(); // and where not ([condition])
Model::find()->andNotFoo()->all(); // and where not ([condition]) , equivalente al llamado anterior
Model::find()->orNotFoo()->all(); // or where not ([condition])

这样,我们就可以看到如何以更灵活的方式使用每个条件,同时流畅的接口允许我们基于基本条件(这些条件将不再重新定义)构建更复杂的组合。

以用户为例,以下我们将看到如何从组合两个基本条件开始,查询一个更复杂的结果

namespace app\models;

use Yii;
use yii\db\ActiveQuery;
use PuntoGAP\YiiConditions\Conditions;

class UsuarioQuery extends ActiveQuery
{
    use Conditions;
    
    protected function conditionTienenTelefono()
    {
        return ['not', ['telefono' => null]];
    }

    protected function conditionTienenEmail()
    {
        return ['not', ['email' => null]];
    }
}
namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Usuario;

class UsuarioController extends Controller
{
    public function actionFoo()
    {
        $usuariosConContacto = Usuario::find()
            ->tienenTelefono()
            ->orTienenEmail()
            ->all();

        $usuariosSinContacto = Usuario::find()
            ->notTienenTelefono()
            ->andNotTienenEmail()
            ->all();
    }
}

从条件定义中链式修饰逻辑

从前面的例子中,我们可以将用户联系信息的查询封装起来,并通过类中另一个方法使其可重用,具体实现如下

namespace app\models;

use Yii;
use yii\db\ActiveQuery;
use PuntoGAP\YiiConditions\Conditions;

class UsuarioQuery extends ActiveQuery
{
    use Conditions;
    
    protected function conditionTienenTelefono()
    {
        return ['not', ['telefono' => null]];
    }

    protected function conditionTienenEmail()
    {
        return ['not', ['email' => null]];
    }

    // Combina las dos condiciones anteriores
    protected function conditionTienenContacto()
    {
        return ['or',
            'tienenTelefono',
            'tienenEmail',
        ];
    }
}

这样,不仅在我们想要查询像前面例子中那样的有联系信息的用户时有所裨益,而且还可以自动获取没有联系信息的用户,如下面的例子所示

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Usuario;

class UsuarioController extends Controller
{
    public function actionFoo()
    {
        $usuariosConContacto = Usuario::find()->tienenContacto()->all();

        $usuariosSinContacto = Usuario::find()->notTienenContacto()->all();
    }
}

这种方式使我们免去了否定条件组合的复杂性,这可能导致错误。同样,我们也抽象掉了查询中涉及的逻辑。其结果之一是,如果某个条件,例如“tienenTelefono”,需要通过不同的方式实施进行修正,那么只需更改这个基本条件的实现,所有其他查询都将自动得到修正。

需要注意的是,当将条件包含在其他条件的定义中时,这些包含的字符串类型条件最终将按照Yii识别的方式来解释为数组类型条件。例如,以下展示了正确和错误包含条件的方式

protected function conditionActivos()
{
    return ['activo' => 1];
}

protected function conditionTienenTelefono()
{
    return ['not', ['telefono' => null]];
}

protected function conditionInactivos()
{
    // 'activos' es equivalente a ['activo' => 1]

    return ['not', ['activos']]; // Forma INCORRECTA, devuelve ERROR
                                 // Es equivalente a ['not', [['activo' => 1]]]

    return ['not', 'activos'];   // Forma CORRECTA
                                 // Es equivalente a ['not', ['activo' => 1]]
}

出于可读性或语义的考虑,甚至可以创建作为其他条件别名的条件。

protected function conditionTienenTelefono()
{
    return ['not', ['telefono' => null]];
}

protected function conditionConTelefono()
{
    return 'tienenTelefono';
}

// ->tienenTelefono() y ->conTelefono() 
// devolverán el mismo query

条件可以无限嵌套,只要逻辑上存在一致的嵌套,并且条件之间的依赖没有形成循环。

直接条件或关系条件

到目前为止,我们已经定义了一种条件类型,即直接条件,它直接应用于相关模型及其在数据库中的对应实体(无论是否执行join、in()等操作,详见观察)。存在基于ActiveRecord中定义的关系构建查询的可能性,这极大地扩展了查询定义的模块化和重用性。

关系条件

关系条件允许我们,给定在ActiveQuery中定义的关系,能够将其用作查询的一部分,以便根据通过该关系存在或不存在相关实例来条件化查询结果。以下将继续以Usuario类为例,查看可能的不同类型的关系条件。

namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use app\models\UsuarioQuery;

class Usuario extends ActiveRecord
{
    // ...

    public static function find()
    {
        return Yii::createObject(UsuarioQuery::className(), [get_called_class()]);
    }

    public function getCiudad()
    {
        return $this->hasOne(Ciudad::class, ['id' => 'ciudad_id']);
    }

    public function getPosts()
    {
        return $this->hasMany(Post::class, ['usuario_id' => 'id']);
    }

    // ...
}

纯关系

这涉及到查询该关系的存在元素,正如其定义的那样。

Foo::find()->withBar()->all();

在前面的Usuario类例子中,我们可以查询有地区分配的用户,或者拥有帖子所有权的用户

use app\models\Usuario;

// Devuelve los usuarios con localidad asignada
Usuario::find()->withLocalidad()->all(); 

// Devuelve los usuarios propietarios de posts
Usuario::find()->withPosts()->all(); 

如我们所见,无论是hasOne还是hasMany关系,都与关系条件兼容(详见与关系条件兼容的关系)。

方法中的条件关系

通过在纯关系条件调用中添加一个关系或一系列纯关系的调用,始终使用驼峰命名法,我们还可以条件化所引用的查询中的关系,同时保持代码的清晰性和易于阅读。

Foo::find()->withBarOneConditionAnotherCondition()->all();

之前我们看到了如何查询具有地区分配的用户。这个查询很可能是多余的,因为通常每个用户都强制性地分配了一个地区。然而,我们可能会遇到以下场景

namespace app\models;

use Yii;
use yii\db\ActiveQuery;
use PuntoGAP\YiiConditions\Conditions;

class LocalidadQuery extends ActiveQuery
{
    use Conditions;

    protected function conditionActivas()
    {
        return ['activa' => 1];
    }

    // ...
}

这个条件允许我们通过Localidad::find()->activas()->all()获取“活跃”的地点,正如我们之前所看到的。但它也自动为我们提供了通过以下方式查询属于活跃地点的用户的能力,即按照前面提到的通过关系名称、条件名称“activas”和camelCase语法连接的方法:

// Devuelve los usuarios pertenecientes a localidades activas
Usuario::find()->withLocalidadActivas()->all(); 

Localidad用单数形式,因为它是关系名称;而Activas用复数形式,因为这对应于条件的声明方式(通常,条件名称按照惯例使用复数形式)。

从调用的方法中获取关系名称和后续条件,是按照首先找到的原则。

参数中的条件关系

如果我们需要自定义正在过滤的关系的条件,可以将该条件作为调用关系条件的第一个参数传递,如下所示:

Foo::find()->withBar($rawCondition)->all();

通过参数传递的条件具有与条件定义返回相同的格式,即一个条件或条件组合的数组。

// Equivalente a la forma general expuesta en el tipo de condición anterior
Foo::find()->withBar(['and', 'oneCondition', 'anotherCondition'])->all();

将前面的地点示例翻译成这种条件形式,我们得到:

// Devuelve los usuarios pertenecientes a localidades activas
Usuario::find()->withLocalidad('activas')->all(); 

尽管这种类型条件的优势在于能够进行更复杂的条件判断,例如:

// Devuelve los usuarios con posts activos o no eliminados
Usuario::find()->withPosts(['or', 
    'activos',
    ['not', 'eliminados']
])->all(); 

调用条件关系的逻辑修饰符

所有关系条件的调用都接受从方法调用到相同逻辑修改器。

->[not]WithRelation...(...)
->[or|and][Not]WithRelation...(...)

例如:

Foo::find()->notWithBar()->all();
Foo::find()->andWithBarOneCondition()->all();
Foo::find()->orNotWithBar('anotherCondition')->all();

与条件关系兼容的关系

可以使用的关联关系可以是简单的,也可以是复合的。如果是复合的,那么无论是使用via还是viaTable方法构建的都兼容。同样,如果关联关系在其实现中进行了查询修改,例如“andWhere”、“andCondition”,这种修改将被保留。相关查询的构建不是通过连接,而是通过嵌套子查询来完成的,这样既可以避免当关系在其实现中进行个性化修改时出现的冲突,也可以避免在hasMany关系中的元素重复问题。

表达式评估条件

通常,只有在满足某些条件时才需要对查询应用条件,或者根据参数进行正反查询,例如在报告生成过程中。

"If" 评估条件

这种条件通过在方法名称后添加“If”后缀来实现。这样做后,方法期望接收一个参数,并且将根据该参数的布尔值接收条件。如果参数返回的值等同于true,条件将被附加到查询上,否则将忽略。

Model::find()->fooIf($condicion)

例如,这个动作只有在请求中请求时才会获取活跃用户。

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Usuario;

class UsuarioController extends Controller
{
    public function actionListarUsuarios()
    {
        $filtrarActivos = Yii::$app->request->get('mostrar_activos');

        $usuarios = Usuario::find()->activosIf($filtrarActivos)->all();
    }
}

"Based on" 评估条件

这种条件通过在方法名称后添加“BasedOn”后缀来实现。与“If”条件类似,它期望接收一个参数,但与“BasedOn”不同,它根据参数的布尔值返回正负条件,如果参数值为空,则忽略条件。视为空值的值包括null和空字符串,或者只包含空格的字符串。其余的假值,如false0[]被视为负值,并将导致条件的否定。

Model::find()->fooBasedOn($condicion)

以下示例说明了根据请求中接收到的参数进行的用户查询的结果。

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Usuario;

class UsuarioController extends Controller
{
    public function actionListarUsuarios()
    {
        // Si llega por ejemplo '1', es de valor positivo
        // Si llega por ejemplo '0', es de valor negativo
        // Si llega por ejemplo '', es de valor es nulo
        $conTelefono = Yii::$app->request->get('con_telefono');

        // Devuelve usuarios "con teléfono" si es de valor positivo
        // Devuelve usuarios "sin teléfono" si es de valor negativo
        // Devuelve todos los usuarios si es de valor nulo
        $usuarios = Usuario::find()->tienenTelefonoBasedOn($conTelefono)->all();
    }
}

列出的所有逻辑修改器都与评估条件兼容。直接条件和关系条件都接受这些条件。以下是一些示例:

Model::find()->andFooIf($condicion)->all();
Model::find()->orNotFooBasedOn($condicion)->all();
Foo::find()->notWithBarIf($condicion)->all();
Foo::find()->withBarOneConditionBasedOn($condicion)->all();
Foo::find()->withBarBasedOn($condicion, 'anotherCondition')->all();

注意:第一个参数始终将是评估条件的条件。在最后一个例子中,可以看到使用条件与条件在参数中的条件关联的参数被移动到第二个位置,而第一个位置留给评估。

将参数传递给条件

我们可能很快就需要定义依赖于某些参数的条件,为此,我们通常会定义一个接受参数作为参数的函数。以下是与条件定义及其调用相关的规范。

  • 可以直接使用必要的参数数量定义直接条件。没有限制,调用时与定义时相同。
// Definición de la condición
protected function conditionFoo($param1, $param2) {
    // ...
}

// Llamado a la condición
Model::foo('val1', 'val2')->all();
  • 可以将参数值传递给定义了这样的条件的条件,即使它们是作为子条件的一部分在另一个条件定义中。在冒号 : 后分配,并用逗号 , 分隔。
// Definición de condición dependiente de foo con parámetros fijos
protected function conditionBar() {
    return 'foo:val1,val2';
}

// Definición de condición dependiente de foo con un parámetro fijo y uno variable
protected function conditionBaz($param1) {
    // Nótese las comillas dobles para la interpolación de $param1
    return "foo:val1,$param1";
}

// Equivale a llamar Model::foo('val1', 'val2')->all();
Model::bar()->all();

// Equivale a llamar Model::foo('val1', 'val3')->all();
Model::baz('val3')->all();
  • 纯关系不带参数。正是由于它们是纯的,这意味着它们不受任何其他标准的条件。唯一可能的情况是使用一个唯一参数,这会自动将条件转换为 参数中的条件关系

  • 在方法中接收条件关系的方法接受必要的参数数量。这些参数将应用于每个条件。在这种情况下使用参数主要是在使用单个条件时有用,这是更常见的情况。如果需要为两个或更多条件提供不同的参数,始终可以选择使用条件数组,其中参数作为子条件附加在冒号 : 之后,如前所述。

  • 当应用“如果”或“基于”的评估条件时,总是将参数移动一个位置,为评估条件的参数留出第一个位置,如前面所述

结果修饰符 "Condition"

如果我们希望从条件调用中获得由 Yii 可解释的 array 结果,请在方法名称末尾添加后缀 Condition。例如

namespace app\models;

use Yii;
use yii\db\ActiveQuery;
use PuntoGAP\YiiConditions\Conditions;

class UsuarioQuery extends ActiveQuery
{
    use Conditions;

    protected function conditionActivos()
    {
        return ['activo' => 1];
    }

    protected function conditionTienenTelefono()
    {
        return ['not', ['telefono' => null]];
    }

    protected function conditionEjemplo()
    {
        return ['and', 'activos', 'tienenTelefono'];
    }

    // ...
}
namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Usuario;

class UsuarioController extends Controller
{
    public function actionFoo()
    {
        // Ejemplo de la condición original
        $condicionPositiva = Usuario::find()->ejemploCondition();
        // Retorna ['and', ['activo' => 1], ['not', ['telefono' => null]]]
        
        // Ejemplo de la condición "negada"
        $condicionNegada = Usuario::find()->notEjemploCondition();
        // Retorna ['not', ['and', ['activo' => 1], ['not', ['telefono' => null]]]]

        // Cualquiera de las condiciones es compatible con ActiveQuery de Yii
        Usuario::find()->where($condicionPositiva)->all();
    }
}

列出的所有 逻辑修饰符 以及 评估条件 都与 结果修饰符 兼容。直接条件和关系条件都接受它们。以下是一些示例

Model::find()->andNotFooCondition();
Model::find()->orFooBasedOnCondition($condicion);
Foo::find()->withBarCondition()->all();
Foo::find()->orNotWithBarBasedOnCondition($condicion, [
    'and', 'oneCondition', 'anotherCondition',
])->all();

whereCondition() 方法

它允许以与 Yii 使用 where() 方法相同的方式评估,但支持“原始条件”。这对于在不需要声明的情况下即时执行复合条件很有用。接受一个带有选项的第二个参数,这些选项可以是

// Activa el modo de condicional de evaluación
$evaluation           // string 'If' | 'BasedOn'

// Activa el modificador de resultado "Condition"
$returnsConditionOnly // bool,

// Asigna el operador en caso de agregar otra condición a una ya existente
$combineWith          // string 'and' | 'or'

也存在 andWhereCondition()orWhereCondition() 方法,类似于框架的本地 andWhere()orWhere()

包含预定义条件

当前软件包包括一个预定义的条件,该条件包含在 Conditions trait 中,因此每个使用此 trait 的查询模型都可以使用它。

->elems($ids)

查找所有记录的类,其主键与通过参数传递的 idid array 对应。符合文档中提到的所有规范。它独立于主键字段的名称。以下是一些示例

// Consulta por el usuario con id = 1
Usuario::find()->elems(1)->one(); // Equivalente a Usuario::findOne(1)

// Consulta por los usuarios que tienen los ids 1 o 2. $filtrar = true, entonces 
// se ejecuta la condición, y el argumento de los ids se desplaza al segundo lugar.
$filtrar = true;
Usuario::find()->notElemsIf($filtrar, [1, 2])->all();

// Consulta los usuarios que correspondan a la localidad con id 1 o 2.
Usuario::find()->withLocalidadElems([1, 2])->all();

// Construye la consulta de los usuarios que, o bien sean activos, o bien sean los
// usuarios con id 3 o 4. Retorna un array compatible con ->where() de ActiveQuery.
$condicion = Usuario::find()->activos()->orElemsCondition([3, 4]);
// Ejecuta la consulta
Usuario::find()->where($condicion)->all();

条件标签

该软件包还包括类似于 ActiveRecord 实现的“属性标签”的条件标签,这对于构建使用条件组合作为过滤器的视图很有用,以执行自定义查询,例如报告。

// Definición de las etiquetas dentro del modelo de consulta
public function conditionsLabels()
{
    return [
        // ...
    ];
}

// Uso de las etiquetas
Model::find()->getConditionLabel($nombreCondicion);

例如:

namespace app\models;

use Yii;
use yii\db\ActiveQuery;

class UsuarioQuery extends ActiveQuery
{
    public function conditionsLabels()
    {
        return [
            'activos' => 'Activos',
            'withLocalidadActivas' => 'Con localidad activa',
        ];
    }
}
Usuario::find()->getConditionLabel('activos'); 
// 'Activos'

Usuario::find()->getConditionLabel('withLocalidadActivas'); 
// 'Con localidad activa'

Usuario::find()->getConditionLabel('tienenTelefono'); 
// 'Tienen Telefono' (interpreta la notación "camel case" cuando no está definida)

使用的术语

  • 原始条件:一个字符串指令或一个包含指令的数组,可以是 ActiveQuery 的本地条件或其他类型的 Condition 条件。
  • 处理条件:一个可以被ActiveQuery的->where()方法解析的数组。

备注

  1. 可以轻松地将正常的Yii实现逐步替换为Yii Conditions,只需在查询模型中包含这个Trait,并将常规函数如ejemplo()逐个替换为其相应的条件函数conditionEjemplo(),返回定义条件的数组而不是查询对象本身。

  2. 请注意在查询模型中定义__call()函数。这个库使用MatiasMuller\MethodsStacks\StackableCall,因此如果已经定义了这个函数,请将其重命名为__callfromLoQueSea,这样它将继续正常工作。如需更多细节,请查看相应文档

  3. 如果需要应用排序或连接,例如,可以在条件定义中进行,只要返回的是“原始条件”的数组。

  4. 为了在所有模型中实现Yii Conditions,建议创建一个通用类,该类扩展自yii\db\ActiveQuery,并在该新类中包含Trait,作为查询模型的基类使用。