2

I am thinking about how to build a large application for a client in Yii2. My experience comes from several smaller Yii2 projects.

What are some major decisions that have to be made during the first steps which cannot be changed easily later on and what are typical Yii2 solution patterns for that?

Here are some features I'm thinking of:

User Administration

A jump start is the extension Yii2-user or Yii2-usario. Gives user management, user login, password reset features and the like.

Multi-tenancy

To manage multiple clients in one database, it is recommended to add the client id to every table and use Yii2 behaviors to add this table field to every database query.

Optional / complex features

Yii2 provides "modules" for code separation. Yii2 modules can have components, models, views, controllers,... and are perfect for delivering independent features at a later stage. Or to separate features from the core application.

Are there similar Yii2 patterns to know about from the start of the project in order to avoid major refactoring during the project?

WeSee
  • 3,158
  • 2
  • 30
  • 58
  • Is there a good solution for integrated oniine help oder manuals? Preferably help where a user can make comments on his/her own? – WeSee Aug 24 '17 at 07:07

2 Answers2

2

An important patter available in Yii2 is for me the RBAC (authorization acces by roles) ..

If the application is large remember you could need Internazionalization for date, currency and formate and multilangue

Audit for check who access to what (there are good extension for this too)

ScaisEdge
  • 131,976
  • 10
  • 91
  • 107
  • Can you help me !! how to do audit in yii2 – Mayank Vadiya Aug 24 '17 at 06:33
  • i have used this and is good for my need https://github.com/bedezign/yii2-audit – ScaisEdge Aug 24 '17 at 06:33
  • I also using same extension but i am new bie for this framework so i can not getting how to use this – Mayank Vadiya Aug 24 '17 at 06:35
  • i have done what in https://bedezign.github.io/yii2-audit/docs/installation/ and work – ScaisEdge Aug 24 '17 at 06:40
  • Internationalization is a good topic:I used an intermediate language as first language (e.g. "button-submit") an then translate this into english ('Submit") and other languages. Why is this not the default language in Yii2? Any reason why this is not recommended? – WeSee Aug 24 '17 at 07:04
  • @scaisEdge I have done this but i don't how to insert trailing and other audit logs in DB – Mayank Vadiya Aug 24 '17 at 07:56
  • @MayankVadiya you should post a specific question about your topic ..eg Yii2 Audit .. – ScaisEdge Aug 24 '17 at 13:17
  • @WeSee Because the english is the most use/common language so this is used as dafualt ..but the logic is the same as what you are trying to do .. – ScaisEdge Aug 24 '17 at 13:18
  • https://stackoverflow.com/questions/45860644/audit-module-cannot-be-loaded-using-yii2-audit @scaisEdge – Mayank Vadiya Aug 24 '17 at 13:18
  • Yii2's RBAC is the biggest issue here. It assumes all tenants will use an identical graph, which is often not the case. The only way around that is to write your own multi-tenant implementation or RBAC. – mae Nov 08 '20 at 20:57
1

in yii2, you can run multiple applications from one codebase. yii2's advanced template gives you great starting point, but you might have more than one "frontend" application. this will help you share or split configurations (including databases) for your frontends. so you can reuse common modules in your applicaions, plus have the freedom to do something completely different.

maybe this is out of scope, but for implementing multi-tenancy i did limit access to data-rows to group members only via behaviour. a where-clause is auto-applied to all selects, so you the client can only return those rows he owns. in your code you can now do select's and join's without having to think about ownership.

ActiveRecord.php

<?php
namespace common\models;

use Yii;
use yii\helpers\Url;

class ActiveRecord extends \yii\db\ActiveRecord
{

    public function behaviors()
    {
        return [
            'group' => [
                'class'     => \x1\data\behaviors\GroupBehavior::className(),
                'map'       => ['gid' => 'group_id'],
                'className' => \common\models\Group::className(),
            ],
        ];
    }


    public static function checkAccess() {
        if (!Yii::$app instanceof \yii\console\Application) {
            $user     = Yii::$app->get('user', false);
            $identity = ($user) ? $user->getIdentity() : null;

            if (empty($identity)) {
                if (!empty($user->loginUrl))
                    return Yii::$app->getResponse()->redirect($user->loginUrl);
                else
                    throw new \yii\web\UnauthorizedHttpException;
            }
        }
    }


    //
    //  select only rows within the user's group,
    //  except for console app
    //
    public static function find() {
        self::checkAccess();
        return (new ActiveQuery(get_called_class()))->current();
    }

}

?>

GroupBehaviour.php

<?php
namespace x1\data\behaviors;

use Yii;
use yii\base\Event;
use yii\db\BaseActiveRecord;

/*

class myModel extends \yii\db\ActiveRecord
{

    public function behaviors()
    {
        return [
            'group' => [
                'class'     => \x1\data\behaviors\GroupBehavior::className(),
                'map'       => ['gid' => 'group_id'],
                'className' => \common\models\Group::className(),
            ],
        ];
    }

}

 */
class GroupBehavior extends \yii\behaviors\AttributeBehavior
{
    public $map       = ['gid' => 'group_id'];
    public $className = null;
    public $value;


    public function getGroup() {
        return $this->owner->hasOne($this->className, $this->map);
    }


    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        if ($this->className == null) {
            throw new \yii\base\InvalidConfigException("'className' must be set");
        }

        if (!is_array($this->map)) {
            throw new \yii\base\InvalidConfigException("'map' must be an array; e.g.: ['gid' => 'group_id']");
        } else {
            if (!count($this->map) > 0) {
                throw new \yii\base\InvalidConfigException("'map' must contain the mapping group => local; e.g.: ['gid' => 'group_id']");
            }
        }

        if (!Yii::$app instanceof \yii\console\Application) {
            if (empty($this->attributes)) {
                $this->attributes = [
                    BaseActiveRecord::EVENT_BEFORE_INSERT => array_values($this->map)[0],
                ];
            }
        }
    }

    /**
     * Evaluates the value of the user.
     * The return result of this method will be assigned to the current attribute(s).
     * @param Event $event
     * @return mixed the value of the user.
     */
    protected function getValue($event)
    {
        if ($this->value === null) {
            $user  = Yii::$app->get('user', false);
            $group = array_keys($this->map)[0];
            return ($user && !$user->isGuest) ? $user->identity->group->$group : null;
        } else {
            return call_user_func($this->value, $event);
        }
    }

}

ActiveQuery.php

<?php
namespace common\models;
use Yii;

class ActiveQuery extends \yii\db\ActiveQuery
{
    private $_alias = null;

    private function getAlias() {

        if ($this->_alias === null) {

            if (empty($this->from)) {
                $modelClass = $this->modelClass;
                $tableName  = $modelClass::tableName();
            } else {
                foreach ($this->from as $alias => $tableName) {
                    if (is_string($alias)) {
                        $this->_alias = $alias;
                        return $this->_alias;
                    } else {
                        break;
                    }
                }
            }

            if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
                $this->_alias = $matches[2];
            } else {
                $this->_alias = $tableName;
            }

        }
        return $this->_alias;
    }

    public function current()
    {
        $alias = $this->getAlias();

        if (!Yii::$app instanceof \yii\console\Application)
            $this->andWhere(['IN', sprintf('COALESCE(%s.group_id,0)', $alias), [0, Yii::$app->user->identity->group_id]]);

        return $this;
    }


    public function rawSql() {
        return $this->prepare(Yii::$app->db->queryBuilder)->createCommand()->rawSql;
    }

}

?>
e-frank
  • 739
  • 11
  • 21
  • Thanks. But wow, that's a lot of code. Why not just using one little behavior in ActiveRecord that checks if the record belongs to the tentant of the user? – WeSee Aug 28 '17 at 11:40
  • i couldn't find a simpler solution - feedback welcome. 1. extend ActiveQuery to auto add a filter per client. 2. register this ActiveQuery in ActiveRecord. GroupBehaviour might not be necessary. – e-frank Aug 29 '17 at 10:44