I'm building an app where clients can create a document from a pre-defined template, edit some fields with their own text and save it. I've sketched out the relations as I think they would be, and it's mostly fine to convert into Laravel:
The only question I have is how I'd handle the FieldValue relationship. The idea is that the Template defines all the fields, then rather than re-create these on each Document, it should just look to its Template for them. That would mean the FieldValue needs to look up to its Document, to the Template of that and find the corresponding Field from there.
Is there a clean way to implement this, or is there a better way of designing the relationship to make it more practical to implement?
Going by your diagram, looks like a pivot table with pivot data...
Which would generally be modeled like this in Laravel:
class Document extends Model
{
public function template()
{
return $this->belongsTo('App\Template');
}
public function fields()
{
return $this->belongsToMany('App\Field')->withPivot('value');
}
}
class Template extends Model
{
public function organisation()
{
return $this->belongsTo('App\Organisation');
}
public function documents()
{
return $this->hasMany('App\Document');
}
public function fields()
{
return $this->hasManyThrough('App\Field', 'App\Section');
}
public function sections()
{
return $this->hasMany('App\Section');
}
}
class Section extends Model
{
public function fields()
{
return $this->hasMany('App\Document')->withPivot('value');
}
public function template()
{
return $this->belongsTo('App\Template');
}
}
class Field extends Model
{
public function documents()
{
return $this->belongsToMany('App\Document')->withPivot('value');
}
public function section()
{
return $this->belongsTo('App\Section');
}
}
class Organisation extends Model
{
public function documents()
{
return $this->hasManyThrough('App\Document', 'App\Template');
}
public function templates()
{
return $this->hasMany('App\Template');
}
}
With related tables (if sticking with laravel defaults):
fields
id - integer
section_id - integer
documents
id - integer
template_id - integer
templates
id - integer
organisation_id - integer
sections
id - integer
template_id - integer
organisations
id - integer
document_field
id - integer
document_id - integer
field_id - integer
value - string
Then you can access things many different ways. Here is one example:
$user = App\User::find(3);
$organisation = $user->organisation;
foreach ($organisation->documents as $document)
{
foreach ($document->fields as $field)
{
echo $field->pivot->value;
}
}
And inserting:
$field = App\Field::find(2);
$document = App\Document::find(4);
$value = 'My field value';
$document->fields()->save($field, ['value' => $value]);
Relevant docs:
Many-to-many relationships
Querying relationships
Inserting related models
Working with pivot tables
Hope this is what you meant:
$doc = Document::findOrFail(Input::get('docId'));
$sections = $doc->template->sections;
$fieldValues = $doc->field values;
Now you simply run on the field values and get the field and the section and start placing stuff.
For better performance I would eager load the fieldValue parameters with:
->with('field');
To give a answer to you're first question:
I suggest that you call it content. You can handle the content as you're doing now. The relationship as is is good enough. You've to do that separated from the other tables.
Second question: a better way
I've created a ERD myself based on you're ERD:
I've changed a few thinks.
Not so important but a document is from a user.
Templates can have sub templates. The consequence of this is that you can reuse sub templates. E.g. if you've a logo you can just place that in you're document every time.
Due to the new template table you don't need sections anymore. With the new approach you can define sub sections/templates.
Properties are all put into one table. With this approach you can define infinite properties for fields. This can be allot more flexible. These are referenced to a field and the content. The content_id however isn't needed. I've just placed it there so you can easily check which field it applies to.
The main point of you're question was the FieldValue/Content. The content is referenced to the document. From the document you can fill in the fields of the template.
I hope this is clear for you. The advantages I see are:
More flexible in properties
Content lookup is easy and referenced to a field
I haven't changed anything on the content table. Just leave it as is. It's good that way! Hope this helps you. If you use Schema designer you can retrieve you're models very easily!
Related
I currently have an Eloquent Model that I have tried to simplify for this example and has a similar structure to the below.
class Student extends Model {
public function classes()
{
return $this->hasMany('App\Classes', 'class_code','code');
}
public function events()
{
return $this->hasOne('App\Events', 'event_code', 'code');
}
}
Every student has an assigned code. Hence a student can be matched to classes or event in a one to many / one relationships via this code. The issue is the relationship of the event. The code is slightly different.
In classes, the code will be 11-ABCD.00
For events, the code is: 11-ABCD
The decimal point is missing in the event code but otherwise, the code is the same. The decimal point simply allows for finer sub-divisions. For relationships, it does not matter and may not always exist i.e. A student may not have a class or event related to them.
I can manually retrieve an Event record like this:
class Student extends Model {
public function events($code)
{
$code = explode('.', $code);
if(count($code) > 0)
{
$code = $code[0];
}
return Event::where('code', $code)->first();
}
}
But this isn't in the true spirit of eloquent when I want to retrieve an entire collection e.g.
$results = Student::with('events')->first();
In short, can I design the relationship of the event to automatically take the key 'code' and strip it so that I can retrieve the records that are relevant?
Example Coding:
Student (Model) (10-ABCD.10)
Classes (One to Many) (10-ABCD.10)
Event (One to One) (10-ABCD)
Reason
I got a legacy system with a table containing slugs.
When those records match, it represents some kind of page with a layout ID.
Because these pages can have different resource needs it depends on the layout ID which tables can be joined with.
I use Laravel's Eloquent models.
What I would like is to have a child model that holds the layout specific relations.
class Page extends Model {
// relation 1, 2, 3 that are always present
}
class ArticlePage extends Page {
// relation 4 and 5, that are only present on an ArticlePage
}
However in my controller, in order to know which layout I need, I already have to query:
url: /{slug}
$page = Slug::where('slug', $slug)->page;
if ($page->layout_id === 6) {
//transform $page (Page) to an ArticlePage
}
Because of this I get an instance of Page, but I would like to transform or cast it to an instance of ArticlePage to load it's additional relations. How can I achieve this?
You'll need to look into Polymorphic relations in Laravel to achieve this. A Polymorphic Relation would allow you to retrieve a different model based on the type of field it is. In your Slug model you would need something like this:
public function page()
{
return $this->morphTo('page', 'layout_id', 'id');
}
In one of your service providers, e.g. AppServiceProvider you would need to provide a Morph Map to tell Laravel to map certain IDs to certain model classes. For example:
Relation::morphMap([
1 => Page::class,
// ...
6 => ArticlePage::class,
]);
Now, whenever you use the page relation, Laravel will check the type and give you the correct model back.
Note: I'm not 100% sure on the parameters etc. and I haven't tested but you should be able to work it out from the docs.
If your layout_id is on the Page model, the only solution I see is to add a method to your Page model that is able to convert your existing page into an ArticlePage, or other page type, based on its layout_id property. You should be able to try something like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
const LAYOUT_ARTICLE = 6;
protected $layoutMappings = [
// Add your other mappings here
self::LAYOUT_ARTICLE => ArticlePage::class
];
public function toLayoutPage()
{
$class = $this->layoutMappings[$this->layout_id];
if (class_exists($class)) {
return (new $class())
->newInstance([], true)
->setRawAttributes($this->getAttributes());
}
throw new \Exception('Invalid layout.');
}
}
What this does is look for a mapping based on your layout_id property, and then it creates a new class of the correct type, filling its attributes with those from the page you're creating from. This should be all you need, if you take a look at Laravel's Illuminate\Database\Eloquent::newFromBuilder() method, which Laravel calls when it creates new model instances, you can see what's going on and how I've gotten the code above. You should be able to just use it like this:
$page = Slug::where('slug', $slug)
->first()
->page
->toLayoutPage();
That will give you an instance of ArticlePage
As far as I know there is no built in function for this.
But you could do something like this.
$page = Slug::where('slug', $slug)->page;
if ($page->layout_id === 6) {
$page = ArticlePage::fromPage($page);
}
And then on the ArticlePage create the static method
public static function fromPage(Page $page)
{
$articlePage = new self();
foreach($page->getAttributes() as $key => $attribute) {
$articlePage->{$key} = $attribute;
}
return $articlePage
}
Depending on your use-case might be smart to create a static method that does this automatically on the relation page() for Slug.
I have a model Page and many models called SomethingSection - they're connected through a polymorphic m-m realtionship and the pivot has an additional column 'position'.
I need to write a relationship (or accessor maybe?) on the Page model that will return a collection of all connected Sections, regardless of their model (read: table).
My models:
class Page extends Model {
public function introSections()
{
return $this->morphedByMany(IntroSection::class, 'pagable');
}
public function anotherSections()
{
return $this->morphedByMany(AnotherSection::class, 'pagable');
}
}
class IntroSection extends Model {
public function pages()
{
return $this->morphToMany(Page::class, 'pagable');
}
}
class AnotherSection extends Model {
public function pages()
{
return $this->morphToMany(Page::class, 'pagable');
}
}
The pivot column looks like this:
pagables
-page_id
-pagable_id
-pagable_type
-position
I'm looking for a way to call a method/attribute on the Page model and get all the connected sections in a single collection, sorted too. What would be a good way to go about this?
I understand that the connected sections do not have the same interface, but in my case that's not a problem at all (in terms of what I will do with the data).
I also understand that relationships perform a separate query (for each relationship), so getting all of them with 1 query is impossible (also different interfaces would be a problem here). And for the same reason the sorting will need to be done on the collection level, not in query.
How could I make this as maintainable as possible and preferably with as small a performance hit as possible.
Thanks in advance.
You can use withPivot() method after your relationship to get the pivot columns with relation like this:
class Page extends Model {
public function introSections()
{
return $this->morphedByMany(\HIT\Models\Sections\IntroSection::class, 'pagable')
->withPivot(['position']);
}
public function anotherSections()
{
return $this->morphedByMany(AnotherSection::class, 'pagable');
}
}
class IntroSection extends Model {
public function pages()
{
return $this->morphToMany(Page::class, 'pagable')
->withPivot(['position']);
}
}
and you can use collection's sortBy to sort the collection by using sortBy() method like this:
$sorted_collection = IntroSection::pages->sortBy('pagables.position');
UPDATE:
You can use collection's combine() method to get all the relationships like this, add this method inside your Page Class:
public function getAllSections()
{
return $this->introSections->combine($this->anotherSections-toArray())
->sortBy('pagables.position'):
}
Hope this helps!
I have a table called payments which contains a field called Vendor ZIP.
I have a table called 201502_postcodes and my "join" in this case is the postcode field in this table.
How do I return field values in this 201502_postcodes table using Eloquent?
My Models are;
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model {
public function postcodeExtract()
{
return $this->belongsTo('App\Models\PostcodeExtract', 'postcode', 'Vendor ZIP');
}
_
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PostcodeExtract extends Model {
protected $connection = 'postcodes';
public function scopeFromTable($query, $tableName)
{
return $query->from($tableName);
}
public function payment()
{
return $this->hasMany('App\Models\Payment', 'Vendor ZIP', 'postcode');
}
So, I have a scope on this model because the 201502 part of my table name is a variable (in that, a new one comes in every quarter).
In my controller... I have no idea what to put. I don't know how to get both scope and relationship to work. How can I write a query that will take a postcode/zip and output one of the fields from the (do I refer to them as "methods"?) postcode extract table?
It is not a duplicate of this question Laravel 4: Dynamic table names using setTable() because relationships are not involved or discussed on that question.
--- UPDATE ---
If I am to use getTable - would it go something like this...
class PostcodeExtract {
public function setTableByDate($selected_tablename)
{
$this->table = $selected_tablename;
// Return $this for method chaining
return $this;
}
public function getTable()
{
if (isset($this->table))
$this->setTableByDate($this->table);
return $this->table;
}
}
And then I would use it in my controller like;
$selected_tablename = 201502_postcode //created by some other controller
$postcode_extract = new PostcodeExtract;
$data = $postcode_extract->setTableByDate($selected_tablename)->get()->toArray();
The Carbon stuff isn't really relevant. I have a lookup to get those tablenames the fact the prefix with a date like value shouldn't mean it's treated like a date.
There are a couple of things going on here.
scopeFromTable() is redundant
Laravel employs magic methods to handle calls to undefined methods. Calling from() on the model will actually call from() on the models internal Query object (assuming you didn't define a method called 'from' on the model itself). It's worth reading the __call and __callStatic methods on the Model class.
relationships use getTable()
Another aspect of the Laravel is the concept of convention over configuration. This basically means that the framework assumes some things so that you don't have to define every detail. In regards to table naming convention, it will naturally use a table name derived from the class name.
// Uses table 'foos'
class Foo {}
There are a few ways to change this behavior. First, you can define a 'table' data member like this.
class Foo {
protected $table = 'bars';
}
If you need a more dynamic behavior, then you can redefine the getTable method.
class Foo {
public function getTable()
{
// return your special table name based on today's date
}
}
Ultimately the models and their relationships refer to getTable to figure out what the table names should be.
your use cases
If you only ever need to query the current table, then I would suggest redefining getTable.
If you need to query both current and past tables, then I suggest pairing a new method along side redefining getTable
class Foo {
public function setTableByDate(\DateTime $date)
{
$this->table = // generate table name from $date
// Return $this for method chaining
return $this;
}
public function getTable()
{
if (isset($this->table))
$this->setTableByDate(\Carbon\Carbon::now());
return $this->table;
}
}
With this in place, you don't have to worry about the table name in your controller or anywhere else unless you need to query past records.
setting the table by date per user
$foos = Foo::setTableByDate($user->some_date)->where(...)->get();
I'm implementing relationships in Eloquent, and I'm facing the following problem:
An article can have many followers (users), and a user can follow many articles (by follow I mean, the users get notifications when a followed article is updated).
Defining such a relationship is easy:
class User extends Eloquent {
public function followedArticles()
{
return $this->belongsToMany('Article', 'article_followers');
}
}
also
class Article extends Eloquent {
public function followers()
{
return $this->belongsToMany('User', 'article_followers');
}
}
Now, when listing articles I want to show an extra information about each article: if the current user is or is not following it.
So for each article I would have:
article_id
title
content
etc.
is_following (extra field)
What I am doing now is this:
$articles = Article::with(array(
'followers' => function($query) use ($userId) {
$query->where('article_followers.user_id', '=', $userId);
}
)
);
This way I have an extra field for each article: 'followers` containing an array with a single user, if the user is following the article, or an empty array if he is not following it.
In my controller I can process this data to have the form I want, but I feel this kind of a hack.
I would love to have a simple is_following field with a boolean (whether the user following the article).
Is there a simple way of doing this?
One way of doing this would be to create an accessor for the custom field:
class Article extends Eloquent {
protected $appends = array('is_following');
public function followers()
{
return $this->belongsToMany('User', 'article_followers');
}
public function getIsFollowingAttribute() {
// Insert code here to determine if the
// current instance is related to the current user
}
}
What this will do is create a new field named 'is_following' which will automatically be added to the returned json object or model.
The code to determine whether or not the currently logged in user is following the article would depend upon your application.
Something like this should work:
return $this->followers()->contains($user->id);