• This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn more.

XF 2.0 Some questions about this whole "Entity" thing...

Jaxel

Well-known member
#1
So entities are a bit confusing to me. I just got used to now XF1 did things, and now XF2 is so completely different. It's my understanding that an "Entity" is basically a combination of the Models and DataWriters of XF1? Is that right? Anyways... the dev guide says things are self-explanatory, but not for me... there seems to be a lot of information unexplained. I'm a little stupid when it comes to the ground level stuff; I'm more into the weeds of of the logic, and less about the semantics.

Here is my first Entity:

Code:
<?php

namespace EWR\Rio\Entity;

use XF\Mvc\Entity\Structure;

class CatLink extends \XF\Mvc\Entity\Entity
{
    public static function getStructure(Structure $structure)
    {
        $structure->table = 'EWRrio_catlinks';
        $structure->shortName = 'EWR\Rio:CatLink';
        $structure->primaryKey = 'catlink_id';
        $structure->columns = [
            'catlink_id' => ['type' => self::UINT, 'autoIncrement' => true],
            'category_id' => ['type' => self::UINT, 'required' => true]
            'game_id' => ['type' => self::UINT, 'required' => true]
        ];
        $structure->getters = [];
        $structure->relations = [
            'Category' => [
                'entity' => 'EWR\Rio:Category',
                'type' => self::TO_ONE,
                'conditions' => 'category_id',
                'primary' => true,
            ],
            'Game' => [
                'entity' => 'EWR\Rio:Game',
                'type' => self::TO_ONE,
                'conditions' => 'game_id',
                'primary' => true
            ],
        ];

        return $structure;
    }
}
My main questions are related to getters, behaviors and relations... mainly, what do they do? What options do I have?

Looking at some entities that already exist, there are some options I've seen, that I don't understand what they do. I see in the relations section, some items that have complicated conditions, "key", and "with"; but no documentation. As well, in what situations would you want to use "self::TO_MANY"? Is it a left to right, or a right to left relation? For instance, in my code above, if a CatLink can only link to one Category, but a Category can link to many CatLinks; which type should I use? As well, in my schema, CatLinks are unique by dual columns category_id+game_id, not to a single Category or Game... so does that change the semantics even more?
 

Jaxel

Well-known member
#2
Second question... I have a table, it has two columns important for this question... "service" and "name".

Column structure in Entities has a new option for "unique". Clearly, this option checks to see if a row you are inserting is unique or not. However, my unique key is not based on a single column, but on the dual column of "service" and "name". In XF1, I would have used a verification function. How would I do this in XF2?
 

katsulynx

Well-known member
#3
The getter part allows you to define additional properties that are not stored in the database. For example you could have a title attribute that is actually a phrase labelled by your add-on id and the entity id. In that case, you can set up your getters property like the following:
Code:
$structure->getters = [
    'title' => true
]
and then implement a function getTitle in your entity that retrieves this part for you.
Code:
public function getTitle() {
    return \XF::phrase('my_addon_id-' . $this->entity_id);
}
After that you can do $entity->title just like you could do with any other database property.


Relations are, as the name implies, relations to other tables. If you do a TO_ONE relation, it's pretty simple. Calling $entity->RelationName will deliver you the entity defined in the relation. Using TO_MANY grants you the opportunity to fetch a number of objects linked to that entity. For example this could be the posts linked to a thread. If you define a TO_MANY relation you can then do $thread->Posts to retrieve their array collection. TO_MANY is always a left-join, anything else wouldn't make sense. You're retrieving the entities that are related to your current entity. If you want to get from Category to CategoryLinks and from CategoryLinks to Category, you will have to define a TO_MANY relation in your Category entity and a TO_ONE relation in your CategoryLinks entity.


If you have an entity with a dual-column index, you just have to define the primaryKey right.
Code:
$structure->primaryKey = ['column_one', 'column_two']
 

Jaxel

Well-known member
#4
Thanks! That was really informative and helpful! Getters and Relations seem extremely useful!

Still a bit stuck on the whole verifying unique dual-columns... they are not a primaryKey.
 

Jaxel

Well-known member
#7
Why aren't they? If it is a relational table, they should be.
Code:
        $this->schemaManager()->createTable('EWRrio_catlinks', function(Create $table)
        {
            $table->checkExists(true);
            $table->addColumn('catlink_id',             'int', 10)->autoIncrement();
            $table->addColumn('category_id',            'int', 10);
            $table->addColumn('game_id',                'int', 10);
            $table->addPrimaryKey('catlink_id');
            $table->addUniqueKey(['category_id', 'game_id'], 'category_id_game_id');
        });
'catlink_id' is primary
['category_id', 'game_id'] is simply unique
 

katsulynx

Well-known member
#8
Entities = DataWriters
Repositories = Models?
There is no such 1<->1 Mapping. It's a new system, consisting out of entities (the most basic structure), finders (the structure that allows you to find and filter entities based on criteria) and the repository which holds a more abstract logic. You'll likely have to distribute the code that you would have placed in a model to all three of these entities.

Basically:
Entity: Stuff around saving, creating, deleting, etc.
Finder: Stuff around retrieving and filtering
Repository: More abstract logic, cache setup, etc.

Code:
        $this->schemaManager()->createTable('EWRrio_catlinks', function(Create $table)
        {
            $table->checkExists(true);
            $table->addColumn('catlink_id',             'int', 10)->autoIncrement();
            $table->addColumn('category_id',            'int', 10);
            $table->addColumn('game_id',                'int', 10);
            $table->addPrimaryKey('catlink_id');
            $table->addUniqueKey(['category_id', 'game_id'], 'category_id_game_id');
        });
'catlink_id' is primary
['category_id', 'game_id'] is simply unique
You can define your uniqueKey as 'catlink_id' and it'll serve the same purpose. There's no need for a unique autoIncrement column in a relational table unless it fulfills some real functionality that the relational key couldn't.
 

Jaxel

Well-known member
#9
You can define your uniqueKey as 'catlink_id' and it'll serve the same purpose. There's no need for a unique autoIncrement column in a relational table unless it fulfills some real functionality that the relational key couldn't.
Well lets say I am editing a channel... in XenForo 1, I would go to the edit link as follows: streams/10/channel/edit

Even though channels are unique by 3 columns (service_id, service_val1, service_val2), its easier to formulate the route action and URL based on a single channel_id autoIncrement. Otherwise the edit link would be as follows: streams/2-UCIW3tvBA80HhhuMwlR9imlA-8wayrun/channel/edit. Not to mention, the extra work of fetching it each time based on 3 variables instead of 1.
 

katsulynx

Well-known member
#11
Not to mention, the extra work of fetching it each time based on 3 variables instead of 1.
Code:
$finder
    ->where('channel_id', $params->channel_id)
    ->fetchOne()
    
$finder
    ->where('service_id', $params->service_id)
    ->where('service_val1', $params->service_val1)
    ->where('service_val2', $params->service_val2)
    ->fetchOne()
Not that much difference. The URL is agreeably quite ugly, but also holds a lot more information on the other hand.
 

Jaxel

Well-known member
#12
If you have a unique constraint that spans multiple columns that are not your primary key then just check for uniqueness in _preSave().
That would probably work:
Code:
    protected function _preSave()
    {
        $exists = $this->finder('EWR\Rio:Spelling')
            ->where([
                'service_id' => $this->service_id,
                'spelling' => $this->spelling,
            ])->fetchOne();
        
        if ($exists && $exists != $this)
        {
            $this->error(\XF::phrase('EWRrio_spelling_already_exists'));
        }
    }
 

Jaxel

Well-known member
#13
I see two different ways to do this same logic:

Code:
    $finder('EWR\Rio:Channel')
        ->where('service_id', $params->service_id)
        ->where('service_val1', $params->service_val1)
        ->where('service_val2', $params->service_val2)
        ->fetchOne();
        
    $finder('EWR\Rio:Channel')
        ->where([
            'service_id' => $params->service_id,
            'service_val1' => $params->service_val1,
            'service_val2' => $params->service_val2,
        ])->fetchOne();
While the first seems much easier to look at... I feel like it wouldn't work with some more complicated AND/OR mixes. In fact, I'm not sure how I would convert the following query using either logic:

Code:
    WHERE EWRporta2_articles.article_date < 1512304516
        AND xf_thread.discussion_state = 'visible'
        AND (
            EWRporta2_articles.article_exclude = '0'
                OR
            EWRporta2_articles.article_exclude IS NULL
        )
 

katsulynx

Well-known member
#14
While the first seems much easier to look at... I feel like it wouldn't work with some more complicated AND/OR mixes. In fact, I'm not sure how I would convert the following query using either logic:

Code:
    WHERE EWRporta2_articles.article_date < 1512304516
        AND xf_thread.discussion_state = 'visible'
        AND (
            EWRporta2_articles.article_exclude = '0'
                OR
            EWRporta2_articles.article_exclude IS NULL
        )
Assuming that WERporta2_articles is the entity you're fetching and has a relation Thread to the xf_thread:
Code:
$finder
    ->where('article_date', '<', 1512304516)
    ->where('Thread.discussion_state', 'visible')
    ->whereOr([
      ['articles_exclude', 0],
      ['articles_exclude', 'IS', 'NULL']
    ]);
should work if I am not mistaken.
 

Jaxel

Well-known member
#15
Assuming that WERporta2_articles is the entity you're fetching and has a relation Thread to the xf_thread:
Code:
$finder
    ->where('article_date', '<', 1512304516)
    ->where('Thread.discussion_state', 'visible')
    ->whereOr([
      ['articles_exclude', 0],
      ['articles_exclude', 'IS', 'NULL']
    ]);
should work if I am not mistaken.
Hopefully that works... I'm at least 2 months away before I'll be able to test it though. Haha.
 

katsulynx

Well-known member
#16
Hopefully that works... I'm at least 2 months away before I'll be able to test it though. Haha.
['articles_exclude', 'IS', 'NULL'] is the only part that I am not 100% certain about, but someone else will probably be able to tell you a way to get it working if it doesn't. You could always do ->whereSql('your query here') as an emergency solution.
 

Jaxel

Well-known member
#17
Okay. I need assistance with another query... In XF1, I used this query to get the full list of "categories" in my DB, but also LEFT JOIN to matching "catlinks" that match a specific "game". This way it gave me the full list of categories, while also telling me which categories a game belonged to (through matching catlinks)
Code:
SELECT *
FROM EWRrio_categories
    LEFT JOIN EWRrio_catlinks
        ON (EWRrio_catlinks.category_id = EWRrio_categories.category_id AND game_id = ?)
ORDER BY EWRrio_categories.category_order ASC, EWRrio_categories.category_name ASC

Its a bit more complicated than a normal JOIN, because there are two equalities in the join. How would I do this with the new finder system?
 

Chris D

XenForo developer
Staff member
#18
That's where TO_MANY Relations might come in (if I've understood the query correctly). You'd have a relation called "Games" which defines how your catlinks entity can get a list of related games.
PHP:
'Games' => [
   'entity' => 'Your:GamesEntity',
   'type' => self::TO_MANY,
   'conditions' => 'category_id',
   'key' => 'game_id'
],
Because you've got a unique key on category_id and game_id, you should be able to use a "key" of "game_id".

You can't actually use an unfiltered TO_MANY relation in a finder query, but you can use a filtered TO_MANY relation. To filter it, you would do this:
PHP:
$finder
    ->with('Games|' . $gameId)
    ->order([
        'category_order',
        'category_name'
    ]);
The query that builds will be more or less identical to your XF1 query (where $gameId is whatever you would have passed into the ? in the XF1 query).
 

Jaxel

Well-known member
#19
I'm still very confused on how to build that query. I'm at a complete loss.

Here is my category entity:
Code:
    $structure->table = 'EWRrio_categories';
    $structure->shortName = 'EWR\Rio:Category';
    $structure->primaryKey = 'category_id';
    $structure->columns = [
        'category_id' =>            ['type' => self::UINT, 'autoIncrement' => true],
        'category_name' =>            ['type' => self::STR, 'required' => true],
        'category_description' =>    ['type' => self::STR, 'required' => true],
        'category_order' =>            ['type' => self::UINT, 'required' => true],
    ];
    $structure->getters = [];
    $structure->relations = [
        'catlink' => [
            'entity' => 'EWR\Rio:CatLink',
            'type' => self::TO_ONE,
            'conditions' => 'category_id',
            'primary' => true,
        ],
    ];

Here is my catlink entity:
Code:
    $structure->table = 'EWRrio_catlinks';
    $structure->shortName = 'EWR\Rio:CatLink';
    $structure->primaryKey = ['category_id', 'game_id'];
    $structure->columns = [
        'category_id'    => ['type' => self::UINT, 'required' => true],
        'game_id'        => ['type' => self::UINT, 'required' => true],
    ];
    $structure->getters = [];
    $structure->relations = [
        'games' => [
            'entity' => 'EWR\Rio:Game',
            'type' => self::TO_MANY,
            'conditions' => 'category_id',
            'key' => 'game_id',
        ],
    ];


I'm as far as this... but naturally it wont work:
Code:
$this->finder('EWR\Rio:Category')
    ->with('catlink', true)
    ->with('games|'.$id)
    ->order('category.category_order')
    ->order('category.category_name')
    ->fetch();
 

Jaxel

Well-known member
#20
I don't even understand how to join tables through a separate table...

Category joins to Catlink
Catlink joins to Game

Category and Game don't share any information, but are joined through the Catlink as an intermediary. The new finder system doesn't seem to facilitate something like that.