• 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...

Chris D

XenForo developer
Staff member
#21
First, we have a convention that is fairly rigid in that relation names should have the first letter capitalised. So your catlink relation should be named "Catlink" and your games relation should be named "Games".

Aside from that, it should be possible to do this, I think:
PHP:
$this->finder('EWR\Rio:Category')
    ->with('Catlink', true)
    ->with('Catlink.Games|'.$id)
    ->order('category_order')
    ->order('category_name')
    ->fetch();
 

Jaxel

Well-known member
#22
Okay, fixed my naming convention... but the code above gives me an error:

XF\Db\Exception: MySQL statement prepare error [1054]: Unknown column 'EWRrio_games_Games_2.category_id' in 'on clause' in src/XF/Db/AbstractStatement.php at line 212

  1. XF\Db\AbstractStatement->getException() in src/XF/Db/Mysqli/Statement.php at line 174
  2. XF\Db\Mysqli\Statement->getException() in src/XF/Db/Mysqli/Statement.php at line 36
  3. XF\Db\Mysqli\Statement->prepare() in src/XF/Db/Mysqli/Statement.php at line 46
  4. XF\Db\Mysqli\Statement->execute() in src/XF/Db/AbstractAdapter.php at line 69
  5. XF\Db\AbstractAdapter->query() in src/XF/Mvc/Entity/Finder.php at line 1135
  6. XF\Mvc\Entity\Finder->fetch() in src/addons/EWR/Rio/Repository/Category.php at line 30
  7. EWR\Rio\Repository\Category->getTest() in src/addons/EWR/Rio/Admin/Controller/Game.php at line 33
  8. EWR\Rio\Admin\Controller\Game->actionEdit() in src/XF/Mvc/Dispatcher.php at line 249
  9. XF\Mvc\Dispatcher->dispatchClass() in src/XF/Mvc/Dispatcher.php at line 89
  10. XF\Mvc\Dispatcher->dispatchLoop() in src/XF/Mvc/Dispatcher.php at line 41
  11. XF\Mvc\Dispatcher->run() in src/XF/App.php at line 1879
  12. XF\App->run() in src/XF.php at line 328
  13. XF::runApp() in admin.php at line 13
 

Jaxel

Well-known member
#23
Oh well... I did it a different way. With an extra query:

Code:
    $categories =
        $this->finder('EWR\Rio:Category')
            ->with('CatLink', true)
            ->where('CatLink.game_id', $id)
            ->order('category_order')
            ->order('category_name')
            ->fetch();
        
    $nonCategories =
        $this->finder('EWR\Rio:Category')
            ->where('category_id', '<>', array_keys($categories->toArray()))
            ->order('category_order')
            ->order('category_name')
            ->fetch();
 

Jaxel

Well-known member
#24
Can I create elements in a finder Object?

So the finder naturally has elements that purport to the column makeup in your database. However, previously in XF1, database queries returned an associative array of data; the finder instead returns objects... which is create, because it keeps joined tables into their own sub objects.

But in XF1 after receiving the data, I would then add new elements to the array. Unfortunately, yo can't really do that with an object, because it violates the object's schema; which is designed to match the column makeup of your database. What can I do to get around this?
 

DragonByte Tech

Well-known member
#25
But in XF1 after receiving the data, I would then add new elements to the array. Unfortunately, yo can't really do that with an object, because it violates the object's schema; which is designed to match the column makeup of your database. What can I do to get around this?
Welcome to my life :p I've got literally tens of thousands of lines of code that may or may not modify an associative array in some way or another across dozens of files, the idea of converting them all to entities makes me want to nope out pretty quick :p

On a slightly more helpful note; the way I see it you have a few options:

1) Convert your entity to an array (either via $entity->toArray(); or by calling $finder->fetchRaw(); instead of just plain fetch) - from the sounds of your post, you'd lose joined row data so this may not be ideal

2) Create getters in your entity that returns the values you'd normally append to the array (this is the preferred way). A word of advice if you do this: Name your getters starting with "get" or "has" or "is". For instance, function getSomeCalculatedValue. If you do not do this, you will be unable to reference your getter in the templates, and you'll be right back to square one. I don't know the full list of accepted prefixes, but those should be safe.

3) Create a separate array with your previously appended elements and pass this to the viewParams. This is sub-optimal because you'll end up with template code like {$newArray.{$entity.entity_id}.value} which looks ugly.


Fillip
 

Jaxel

Well-known member
#26
2) Create getters in your entity that returns the values you'd normally append to the array (this is the preferred way). A word of advice if you do this: Name your getters starting with "get" or "has" or "is". For instance, function getSomeCalculatedValue. If you do not do this, you will be unable to reference your getter in the templates, and you'll be right back to square one. I don't know the full list of accepted prefixes, but those should be safe.
Do you have any examples of these setters and getters?
 

DragonByte Tech

Well-known member
#27
Do you have any examples of these setters and getters?
Certainly :)

You don't need a setter in your entities, unless you need to add an option. For instance, consider the following structure definition:
PHP:
        $structure->columns = [
            'license_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true, 'changeLog' => false],
            'product_id' => ['type' => self::UINT, 'required' => true, 'default' => 0,
                'verify' => 'verifyProduct'
            ],
            'user_id' => ['type' => self::UINT, 'required' => true],
            'purchase_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'expiry_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'license_key' => ['type' => self::STR, 'readOnly' => true],
            'custom_fields' => ['type' => self::SERIALIZED_ARRAY, 'default' => [], 'changeLog' => 'customFields'],
        ];
Notice 'license_key' => ['type' => self::STR, 'readOnly' => true], - this means if you try to do $entity->license_key = 'X'; you'll get an error message.

However, you can do this: $entity->set('license_key', 'X', ['forceSet' => true]); to override the readOnly property.

As for getters, the following is taken from \XF\Entity\User:
PHP:
    public function getAvatarType()
    {
        if ($this->gravatar && $this->app()->options()->gravatarEnable)
        {
            return 'gravatar';
        }
        else if ($this->avatar_date)
        {
            return 'custom';
        }
        else
        {
            return 'default';
        }
    }
In XF1, you might have had a block of code that set $user['avatar_type'] whenever the user object was fetched, but in XF2 you use a getter like this.

Furthermore, you can explicitly define getters to refer to functions instead of column values. For instance, if you have this structure:
PHP:
        $structure->getters = [
            'is_super_admin' => true
        ];
Then the corresponding getter would be:
PHP:
    /**
     * @return bool
     */
    public function getIsSuperAdmin()
    {
        if ($this->Admin)
        {
            return $this->Admin->is_super_admin;
        }

        return false;
    }
As you will have noticed from the function name, it takes the $structure->getters array key and turns it into CamelCase, and puts "get" in front of it. is_super_admin -> getIsSuperAdmin, avatar_type -> getAvatarType, etc.

The benefit of doing this is that every time you call $entity->is_super_admin, the getIsSuperAdmin function is actually called instead, which is what I assume you will want when it comes to adding new columns.

In short: Take the new array keys you wanted to add to the array (which is now an Entity), define them as getters in your entity structure, and do whatever calculations you normally did in-line in the getter function instead.

Hope that made sense, it is past midnight here :D


Fillip
 

Jaxel

Well-known member
#28
Why don't you need a setter? This is what I am doing right now:

Code:
    public function getStreamUrl()
    {
        return $this->_getterCache['stream_url'];
    }
   
    public function getStreamVideo()
    {
        return $this->_getterCache['stream_video'];
    }
   
    public function getStreamChat()
    {
        return $this->_getterCache['stream_chat'];
    }
   
    public function setStreamDetails($url, $video, $chat)
    {
        $this->_getterCache['stream_url'] = $url;
        $this->_getterCache['stream_video'] = $video;
        $this->_getterCache['stream_chat'] = $chat;
    }
   
    public static function getStructure(Structure $structure)
    {
        $structure->table = 'EWRrio_streams';
        $structure->shortName = 'EWR\Rio:Stream';
        $structure->primaryKey = ['service_id', 'stream_value1', 'stream_value2'];
        $structure->columns = [
            ...
        ];
        $structure->getters = [
            'stream_url' => 'StreamUrl',
            'stream_video' => 'StreamVideo',
            'stream_chat' => 'StreamChat',
        ];

        return $structure;
    }

If you didn't have a setter, how would you set the values?
 

Jaxel

Well-known member
#29
If I simply try:
Code:
$stream->set('stream_url', $newValue);

I get an error about 'stream_url' not existing...
 

DragonByte Tech

Well-known member
#30
Why don't you need a setter? This is what I am doing right now:
Gods no, get that out of your code. I would recommend looking at the entities already present in XenForo.

I don't know what you're doing with the "getterCache" but if that is something you found deep down in XF's core code, you shouldn't mess with it. You need to throw all of that out and start again.

The point I was trying to make with my previous post was that if "stream_chat" was a calculated value from the existing database data, you'd use a getter to compute and return the data on the fly.

If stream_chat is a column from another table or something, you want a relation instead.


Fillip
 

Jaxel

Well-known member
#31
Gods no, get that out of your code. I would recommend looking at the entities already present in XenForo.

I don't know what you're doing with the "getterCache" but if that is something you found deep down in XF's core code, you shouldn't mess with it. You need to throw all of that out and start again.

The point I was trying to make with my previous post was that if "stream_chat" was a calculated value from the existing database data, you'd use a getter to compute and return the data on the fly.

If stream_chat is a column from another table or something, you want a relation instead.


Fillip
Well 'stream_chat' is a calculated value. However, those calculations are shared with 'stream_url' and 'stream_video' as well... if I did it would a getter, that would do the calculation 3 times, instead of only once. That doesn't seem like very efficient use.
 

DragonByte Tech

Well-known member
#32
Well 'stream_chat' is a calculated value. However, those calculations are shared with 'stream_url' and 'stream_video' as well... if I did it would a getter, that would do the calculation 3 times, instead of only once. That doesn't seem like very efficient use.
Then you could use a regular property to store the result of the calculation, e.g.
PHP:
protected $streamChat;

public function getStreamChat()
{
    if ($this->streamChat === NULL)
    {
        // Calculate streamchat
        $streamChat = 'something';
        
        $this->streamChat = $streamChat;
    }

    return $this->streamChat;
}
From the looks of your example previously, you were doing the calculations for streamChat outside of the entity and setting it into the entity, and that's the difference - with the entities, you're trying to make your code as portable and reusable as possible by putting these kind of calculations inside the entity.

Since you're essentially creating a fake property, pretending stream_chat is a column in your database, you'd do the calculations inside the entity. That means if I wanted to integrate with your mod, I wouldn't need to copy all your OTHER code in order to get the necessary data to calculate stream_chat, then set it into the entity, then get it later in the template.

That leads to code duplication as soon as you need to access your Stream entity in more than one location :)


Fillip
 

Jaxel

Well-known member
#33
Then you could use a regular property to store the result of the calculation, e.g.
PHP:
protected $streamChat;

public function getStreamChat()
{
    if ($this->streamChat === NULL)
    {
        // Calculate streamchat
        $streamChat = 'something';
       
        $this->streamChat = $streamChat;
    }

    return $this->streamChat;
}
From the looks of your example previously, you were doing the calculations for streamChat outside of the entity and setting it into the entity, and that's the difference - with the entities, you're trying to make your code as portable and reusable as possible by putting these kind of calculations inside the entity.

Since you're essentially creating a fake property, pretending stream_chat is a column in your database, you'd do the calculations inside the entity. That means if I wanted to integrate with your mod, I wouldn't need to copy all your OTHER code in order to get the necessary data to calculate stream_chat, then set it into the entity, then get it later in the template.

That leads to code duplication as soon as you need to access your Stream entity in more than one location :)


Fillip
Okay, where would I do those calculations in the Entity, so that I can fetch any of those 3 values, in any order?
 

DragonByte Tech

Well-known member
#34
Okay, where would I do those calculations in the Entity, so that I can fetch any of those 3 values, in any order?
If you always need all 3 variables, then I'd probably just consider a single getter method and returning an array, e.g. return ['url' => 'url value', 'video' => 'video value', 'chat' => 'chat value'];


Fillip
 

Jaxel

Well-known member
#35
If you always need all 3 variables, then I'd probably just consider a single getter method and returning an array, e.g. return ['url' => 'url value', 'video' => 'video value', 'chat' => 'chat value'];


Fillip
That doesn't quite cut it for me.... nor would it give access to it in templates.
 

DragonByte Tech

Well-known member
#37
That doesn't quite cut it for me.... nor would it give access to it in templates.
Yes it would, you are able to call getter methods on entities in the templates :)
That's part of what makes the entity system so good.

<xf:if is="{{ $xf.visitor.hasPermission('general', 'editCustomTitle') }}">

This is taken from the account_details template. $xf.visitor is an instance of \XF\Entity\User


Fillip
 

Jaxel

Well-known member
#38
So then theoretically, I could do this?
Code:
<xf:if is="{{ $stream.autoplay('true').stream_url }}">
Assuming my getter is named "getAutoplay"
 

DragonByte Tech

Well-known member
#39
What about passing a value into the calculations?
Sorry, I don't understand the question.

If you need to have a closer look at how the entities in XF2 are built, I'd advise you to open src/XF/Entity and have a gander at the files in there :)

Whenever I wonder about the best way of going about something, or how something is done in XF2, I ask myself; where does XF2 do what I need (or closest to what I need)?

Then I dig through the code (a good IDE like VSCode or phpStorm helps here) and work out how to construct my own code to most closely follow the XF2 conventions.

So then theoretically, I could do this?
Code:
<xf:if is="{{ $stream.autoplay('true').stream_url }}">
Assuming my getter is named "getAutoplay"
I'm not 100% sure if you can directly use the return values like that. I would do this:

Code:
<xf:set var="$autoPlay" value="{{ $stream.getAutoPlay('true') }}" />
<xf:if is="{{ $autoPlay.stream_url }}">
(Rename your getter to getAutoPlay for the reasons I mentioned previously.)


Fillip
 

Jaxel

Well-known member
#40
I need to pass a value for "autoplay" (true or false).

Do some calculations... then return the values.

I have to do these calculations for three pieces of data.

I need to use this returned data in both code, and in templates.

I would rather not have to do the calculations 3 separate times, but one time, for all three pieces of data.

How do I do this?

This worked:
Code:
{{ $stream.getAutoplay(true).stream_url }}
But doing this would do the calculates three times... which isn't good.
Code:
{{ $stream.getAutoplay(true).stream_url }}
{{ $stream.getAutoplay(true).stream_video }}
{{ $stream.getAutoplay(true).stream_chat }}