Extending API with MemberUser example

Our current UserFrosting project is our first with Laravel/Eloquent so we’re still learning.

We got a little cocky after some early Eloquent ORM model work adding tables and joins to achieve some additional functionality. But when we tried to do a hasOne (1:1) relationship following the MemberUser example, we seem to have failed miserably. So, we sidelined all that code into a holding git branch, and pulled in the EXACT code in the ExtendUser branch (making only minimal changes to table fields).

Following the MemberUser model extension for Eloquent, most tested functionality is working as expected (Trait handlers for save() are creating Members, etc.) so the code seems to be working. But we presumed that the API calls would return the MemberUser model in JSON (or CSV) via the toResponse->withJson call. But they don’t. A call to site.com/api/users returns only the original Users fields (despite the hasOne relationship in MemberUser and the leftJoin in scopeJoinUser in Member). And, yes. The endpoint routings are updated, the Class Mapper is substituting the MemberUser class for the User class (all verified by debug and the Trait links testing.)

What are we overlooking?

So to get the related entities, you have a couple options:

  • You can add ->with('member') to your Sprunje’s query to eager-load the related entity. Keep in mind that if you do this, you’ll have to access the fields in this table via the member relation. You’ll also have to add custom filters in your Sprunje and use a whereHas closure to be able to filter on these fields;
  • You can use a global scope to automatically join the user table fields whenever you access a Member object. This is basically what the scopeJoinUser does, except it is automatically applied to any queries off the Member model. This is probably my favorite approach, and I actually use it to join our workers’ account balances when we retrieve them:
<?php
/**
 * Bloomington Tutors
 *
 * @link      https://bitbucket.org/bloomingtontutors/btoms
 * @license   All rights reserved
 */
namespace UserFrosting\Sprinkle\Btoms\Database\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class AccountScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        // Determine the type of entity that has this account
        $morphType = $model->getMorphClass();
        $primaryKeyName = $model->getQualifiedKeyName();
        $table = $model->getTable();

        $builder->addSelect("$table.*", 'accounts.balance as balance');

        // Join on matching primary accounts
        $builder->leftJoin('accounts', function ($join) use ($primaryKeyName, $morphType) {
            $join->on('accounts.accountable_id', '=', $primaryKeyName)
                 ->where('accounts.accountable_type', '=', $morphType)
                 ->where('accounts.type', '=', 'primary');
        });
    }
}

You’ll notice that in this example, we dynamically decide which table to join on, because there are multiple types of entities that could have accounts. Once you have this scope defined, you need to boot it in your Member model:

    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new AccountScope);
    }

So, you’d probably do something similar, joining on your members table. I might actually change the documentation to recommend this global scope approach over the local scopeJoinUser approach.

The other thing we’re working on is support for properties in Sprunjes. So, you’d be able to specify properties[]= in a request to automatically load relations in a Sprunje.

1 Like

Thanks, Alex. I’ll metabolize this and pass it along to development. I’d agree with the idea of changing the documentation (and recommendation) to the global scope approach as I can attest that it was the scopeJoinUser that nudged us toward a conviction that the the virtual model should be complete for the getList calls.

Whether best practice or not, we did gravitate toward using the API calls to validate whether we had models and joins working as designed as we progressed in our Eloquent learning curve. So we chased logic errors until they were just not evident or likely before asking this question.

This would help others who may encounter similar, apparent, logic errors that are really just lack of understanding of the underlying frameworks (and some degree of failure to research those frameworks for answers.)

Thanks, BTW. This helps keep us from chasing our tails.

Thinking through a couple more things that may help you in documentation to see the level of naivete working as teams adopt UserFrosting, the examples are an understandably excellent way for most coders to quickly assimilate concepts. One way that the Virtual Model example may have added to our confusion is the description of the virtual model:

To bring the two entities together we’ll create a third model, MemberUser, which extends the base User model to make it aware of the Member. This virtual model will enable us to interact with columns in both tables as if they were part of a single record.

From this, seemingly innocuous, and very helpful description we assumed that the virtual model, combined with the Eloquent joins and hasOne relations were all that should be needed to represent a complete Virtual Model table and return its fields in a ->toArray withJson construct.

Gotcha. Yeah to be honest, I was only just figuring out some of these features of Eloquent myself when I wrote this guide. When I get some time I can update them to reflect what I’ve learned since then.

1 Like

Alex - we’ve been working through how we plan to handle polymorphic relations and multi-table data reservoirs and an obvious question was raised: “why don’t we push multi-table reservoirs back to the database behind the abstraction layers?” So, have you thought about the use of database views to, essentially, a U b tables that are 1:1 extensions of one another? Eloquent supports this through Laravel and it seems to us that it allows the virtualization of a multi-table representation of all reservoir columns using simpler syntax and readable code.

Thoughts?

See here: Eloquent and SQL views

For some reason I’ve always shied away from SQL views - I’m not really sure why. I think my reasoning was that it would make the database implementation less portable, but I’m not even sure if that’s really true. I’d love if someone could explore this option. Moving multi-table interfaces out of the application layer and into the database makes a lot of sense to me.

Yeah. I think it does to me, too. We haven’t worked through all the implications, and one of the big ones is being clear with framework adopters about the limitations of views (read only). But given that the write requirements are, essentially, the same, the questions about pushing multi-table back to the database focus on two or three areas of concern that I can think of right now: 1) portability across database providers (so far all of Laravel/Eloquent’s supported database providers also support views); 2) determining where separate logic will be needed on the client side to facilitate writes (this seems relatively a small concern, if not already inconsequential, given the sequestration of code for gets and puts in most MVC controllers setups; and 3) determining similar requirements on the server side, including scaling and engaging foreign data stores (or leaving that option available for other teams that may want to use it.)

I’ve reworked the extend-user Sprinkle to use global scopes, and a simpler way to deal with basic CRUD for extended models. For the time being, this is on the develop branch: https://github.com/userfrosting/extend-user/tree/develop

Basically, I’ve renamed Member => MemberAux and MemberUser => Member to make things clearer. The members table no longer has a separate column for its FK to the users table - instead I’m just using the id column of members as both its PK and FK.

I’ve also implemented the MemberAuxScope global scope, which automatically joins the users and members table.

Once I update the documentation, I can merge this into master.

Alright, these features have now been released with v4.1.1 of extend-user, and the documentation has been updated as well! Enjoy!

1 Like