Created
April 8, 2019 17:45
-
-
Save Anubarak/b404fc7d115a6290164abacc7c1628ca to your computer and use it in GitHub Desktop.
Revisions
-
Anubarak created this gist
Apr 8, 2019 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,75 @@ <?php // just a simple migration file to store the information namespace craft\contentmigrations; use Craft; use craft\db\Migration; /** * m190408_152357_ratings migration. */ class m190408_152357_ratings extends Migration { public $tableName = '{{%user_ratings}}'; /** * @inheritdoc */ public function safeUp() { // Place migration code here... $this->createTable( $this->tableName, [ 'id' => $this->primaryKey(), 'elementId' => $this->integer()->notNull(), // the element id that is voted 'siteId' => $this->integer()->notNull(), // the site id, to make it complete 'userId' => $this->integer()->notNull(), // the ID of the user that voted, just as an example 'rating' => $this->integer(), // just a number from 0-X 'text' => $this->text(), // some comment 'dateUpdated' => $this->dateTime()->notNull(), 'dateCreated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), ] ); $this->createIndex(null, $this->tableName, 'userId'); $this->addForeignKey( null, $this->tableName, ['userId'], '{{%elements}}', ['id'], 'CASCADE' ); $this->addForeignKey( null, $this->tableName, ['siteId'], '{{%sites}}', ['id'], 'CASCADE' ); $this->addForeignKey( null, $this->tableName, ['elementId'], '{{%elements}}', ['id'], 'CASCADE' ); } /** * @inheritdoc */ public function safeDown() { $this->dropTable($this->tableName); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,101 @@ <?php // just the basic module stuff for events and such class Module extends \yii\base\Module { /** * @var \modules\Module $instance */ public static $instance; public function __construct($id, $parent = null, array $config = []) { // ... // some controller initialization and all the things // ... // define a behavior for all elements, let's call it rating Event::on( Element::class, Element::EVENT_DEFINE_BEHAVIORS, static function(DefineBehaviorsEvent $event) { $event->behaviors['ratingBehavior'] = RatingBehavior::class; } ); // define a query behavior for all ElementQueries Event::on( ElementQuery::class, ElementQuery::EVENT_DEFINE_BEHAVIORS, static function(DefineBehaviorsEvent $event) { $event->behaviors['ratingQueryBehavior'] = RatingQueryBehavior::class; } ); // should your attributes appear in the index list? Event::on( Element::class, Element::EVENT_REGISTER_TABLE_ATTRIBUTES, [$this, 'registerTableAttributes'] ); // set the HTML Event::on( Element::class, Element::EVENT_SET_TABLE_ATTRIBUTE_HTML, [$this, 'setTableAttribute'] ); // wanna make them sortable? Event::on( Element::class, Element::EVENT_REGISTER_SORT_OPTIONS, [$this, 'registerSortOption'] ); // just a way to apply eager loading.. you can as well create a Twig extension Event::on( CraftVariable::class, CraftVariable::EVENT_INIT, static function(Event $event) { /** @var CraftVariable $variable */ $variable = $event->sender; $variable->set('foobar', Variable::class); } ); // .... } /** * registerTableAttributes * * @param \craft\events\RegisterElementTableAttributesEvent $event * * @author Robin Schambach */ public function registerTableAttributes(RegisterElementTableAttributesEvent $event) { $event->tableAttributes['rating'] = [ 'label' => \Craft::t('app', 'Rating') ]; } /** * setTableAttribute * * @param \craft\events\SetElementTableAttributeHtmlEvent $event * * @author Robin Schambach */ public function setTableAttribute(SetElementTableAttributeHtmlEvent $event) { if ($event->attribute === 'rating') { $event->html = $event->sender->rating; } } public function registerSortOption(RegisterElementSortOptionsEvent $event) { $event->sortOptions['rating'] = \Craft::t('app', 'rating'); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,206 @@ <?php /** * craft for Craft CMS 3.x * * Created with PhpStorm. * * @link https://github.com/Anubarak/ * @email [email protected] * @copyright Copyright (c) 2019 Robin Schambach */ namespace modules\behaviors; use craft\base\Element; use craft\elements\User; use craft\events\ModelEvent; use yii\base\Behavior; /** * Class RatingBehavior * @package modules\behaviors * @since 08.04.2019 * @property array $attributes * @property \craft\elements\User|null $user * @property Element $owner */ class RatingBehavior extends Behavior { /** * The ID of our user rating "record" * * @var int $id */ public $userRatingId; /** * Note: you might want to rename all these since it's highly likely they'll * be already used as field handles.. I just didn't want to use variables like * `userRatingUserId` for this example * * As you may have noticed this is just a simple use case * every element can only be rated once.. not really realistic, it's just * to show you the basic idea * * @var int $userId */ public $userId; /** * The user that created the comment * * @var User $_user */ private $_user; /** * The element that will be rated, basically the same as `$this->owner` * * @var \craft\base\Element $element */ public $element; /** * How many points should the element receive, rating from 0-10000000 * * @var int $rating */ public $rating; /** * Some additional text or other information about users the opinion * * @var string $text */ public $text; /** * events * * @return array * * @author Robin Schambach */ public function events(): array { return [ Element::EVENT_AFTER_SAVE => 'afterSaveElement', ]; } /** * afterSaveElement * * @param \craft\events\ModelEvent $event * * @throws \yii\db\Exception * * @author Robin Schambach */ public function afterSaveElement(ModelEvent $event) { // everything is called by reference so luckily it doesn't care // if we use $event->sender or this :) $data = [ 'elementId' => $this->owner->id, 'userId' => $this->userId, 'rating' => $this->rating, 'text' => $this->text, 'siteId' => $this->owner->siteId ]; // validate the data somehow... for this example I'll just do a crappy if statement if($data['userId'] !== null && $data['siteId'] !== null && $data['elementId'] !== null){ // insert or update the data as you like :) $isNew = $this->userRatingId === null; if ($isNew === true) { // insert a new value \Craft::$app->getDb()->createCommand()->insert('{{%user_ratings}}', $data)->execute(); } else { // update an existing one \Craft::$app->getDb()->createCommand() ->update('{{%user_ratings}}', $data, ['id' => $this->userRatingId])->execute(); } // include some error handling.. I'll leave it here as it is } } // some setters and getters... mostly boring stuff /** * setRating * * @param int $rating * * @return $this * * @author Robin Schambach */ public function setRating(int $rating): self { $this->rating = $rating; return $this; } /** * setUser * * @param \craft\elements\User $user * @return \craft\base\Element * * @author Robin Schambach */ public function setUser(User $user): Element { $this->userId = $user->id; $this->_user = $user; return $this->owner; } /** * setUserId * * @param int $userId * @return \craft\base\Element * * @author Robin Schambach */ public function setUserId(int $userId): Element { $this->userId = $userId; return $this->owner; } /** * setText * * @param string $text * @return \craft\base\Element * * @author Robin Schambach */ public function setText(string $text): Element { $this->text = $text; return $this->owner; } /** * Get the actual User object * * @return \craft\elements\User|null * * @author Robin Schambach */ public function getUser() { if($this->_user === null){ $this->_user = \Craft::$app->getUsers()->getUserById($this->userId); } // you might wonder: uhh tooo many queries... // but we'll implement eager loading later on^^ return $this->_user; } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,143 @@ <?php /** * craft for Craft CMS 3.x * * Created with PhpStorm. * * @link https://github.com/Anubarak/ * @email [email protected] * @copyright Copyright (c) 2019 Robin Schambach */ namespace modules\behaviors; use craft\elements\db\ElementQuery; use craft\events\PopulateElementEvent; use craft\helpers\Db; use yii\base\Behavior; /** * Class RatingQueryBehavior * @package modules\behaviors * @since 08.04.2019 * @property ElementQuery $owner */ class RatingQueryBehavior extends Behavior { /** * Basically the same as Craft does * * @var integer $rating */ public $rating; /** * rating * see Craft docs * * Entry::find()->rating('>4'); * Entry::find()->rating(['and', '>4', '<7']) * everything is possible here :) * * @param int $rating * @return \craft\elements\db\ElementQuery * * @author Robin Schambach */ public function rating($rating): ElementQuery { $this->rating = $rating; return $this->owner; } /** * events * * @return array * * @author Robin Schambach * @since 08.04.2019 */ public function events(): array { return [ ElementQuery::EVENT_AFTER_PREPARE => 'onAfterPrepare', // this is a bit hacky and only for lazy people that don't want to trigger a custom Controller // I never actually used it that way, so I didn't spend much time thinking about a better way // Maybe we could ask Brad/Brandon for a custom event specific to populate custom values by post request // you could as well use `beforeValidate` events.... ElementQuery::EVENT_AFTER_POPULATE_ELEMENT => 'afterPopulateElement' ]; } /** * onAfterPrepare * * @author Robin Schambach * @since 08.04.2019 */ public function onAfterPrepare() { // join it for both because we might want to filter later on.. $this->owner->subQuery->leftJoin('{{%user_ratings}} ratings', '[[ratings.elementId]] = [[elements.id]]'); $this->owner->query->leftJoin('{{%user_ratings}} ratings', '[[ratings.elementId]] = [[elements.id]]') // select all the additional columns ->addSelect( [ 'ratings.userId', 'ratings.rating', 'ratings.text', 'ratings.id as userRatingId', // "id" will cause conflicts ] )// search for the correct site, note this might change after Craft 3.2 when they query for multiple sites ->andWhere( [ 'or', ['ratings.siteId' => $this->owner->siteId], // always include a null row for reasons... you won't be able to login otherwise because no user is found :P ['ratings.siteId' => null] ] ); // include custom conditions if($this->rating !== null){ // only query for ratings with the criteria $this->owner->subQuery->andWhere(Db::parseParam('ratings.rating', $this->rating)); } // include some other custom conditions.... } /** * AfterPopulateElement * * @param \craft\events\PopulateElementEvent $event * * @throws \craft\errors\SiteNotFoundException * * @author Robin Schambach */ public function afterPopulateElement(PopulateElementEvent $event) { /** @var \craft\base\Element $element */ $element = $event->element; // at this point our element already has all the attributes from DB populated // as said in the comment few lines above, this is actually a hacky way... and doesn't allow // you to store your custom fields from the beginning but usually you want to handle such thing via custom // controller action ¯\_(ツ)_/¯ or use some other event for this // you can as well do that in beforeSave or where-ever you want $request = \Craft::$app->getRequest(); if ($request->isConsoleRequest === false && $request->getIsPost() === true) { $siteId = (int) $request->getBodyParam('siteId', \Craft::$app->getSites()->getCurrentSite()->id); if ($element->id !== null && (int) $element->siteId === $siteId && (int) $element->id === (int) $request->getBodyParam('elementId')) { // the element matches with the one of our post request, populate the new values $element->setRating($request->getBodyParam('rating', $element->rating)) ->setUserId($request->getBodyParam('userId', $element->userId)) ->setText($request->getBodyParam('text', $element->text)); } } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,41 @@ <?php /** * craft for Craft CMS 3.x * * Created with PhpStorm. * * @link https://github.com/Anubarak/ * @email [email protected] * @copyright Copyright (c) 2019 Robin Schambach */ namespace modules; use craft\elements\User; use craft\helpers\ArrayHelper; class Variable { /** * Just a real crappy version of eager loading just to show the basic idea * this should usually be in a component/service with a bit more stuff in it * @param array $elements * * @author Robin Schambach */ public function eagerLoadUser(array $elements) { // fetch all unique user Ids $userIds = array_unique(ArrayHelper::getColumn($elements, 'userId')); // fetch all user indexed by ID $users = ArrayHelper::index(User::find()->id($userIds)->all(), 'id'); foreach ($elements as $element){ // set the user to the element $user = $users[$element->userId]?? null; if($user !== null){ $element->user = $user; } } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,46 @@ {# just a simple use case in Twig how to display the information and store content of an element #} {# show some attributes #} {{ entry.text }} {{ entry.rating }} {{ entry.user }} {# change some attributes #} <form method="post"> {{ csrfInput() }} <input type="hidden" name="action" value="entries/save-entry"> {# of course we would usually hash the value and all this kind of things.. just not now #} <input type="hidden" name="elementId" value="{{ entry.id }}"> {# same as well... we would usually grab that via PHP or hash it, but I'm lazy in this example #} <input type="hidden" name="userId" value="{{ currentUser.id }}"> {# the id for Crafts internal Controller #} <input type="hidden" name="entryId" value="{{ entry.id }}"> How much do you like it? <input type="number" name="rating" value="{{ entry.rating }}"> Tell us your opinion <textarea name="text" cols="30" rows="10">{{ entry.text }}</textarea> <input type="submit">Submit it </form> {# display a list #} {# see RatingQueryBehavior for information about the query, notice the custom rating() function #} {% set entries = craft .entries .rating('>2') .all() %} {# apply eager loading for users #} {% do craft.foobar.eagerLoadUser(entries) %} {% for entry in entries %} ---------------------------------- title {{ entry.title }}<br> points: {{ entry.rating }}<br> message: {{ entry.text }}<br> user: {{ entry.user }} ----------------------------------- {% endfor %}