Eloquent Relationships - php

Long story short: I'm building a "privacy" page where uses can chose what shows up and what does not show up on their profiles.
I am considering having a 1:m table user:privacy and just have entries for the keys they want private. If they don't exist they are public. Hope this makes sense.
Table would be user_privacy and will have 3 columns: id, user_id, privacy_key (string, i.e. email/phone/cell/etc)
Is there a way to simple query by the keys i will define that i can run to determine if the user has a key or not or do i have to go extra lengths to add a function to the user model to do this (trying to avoid, love the magic-ness of eloquent)
Basically i want to have a condition that sounds like "if ($user->privacy->email or $user->privacy->phone)"
Thanks and hope i was clear enough, lol

You could add a function to your user model:
public function isPrivate($attribute){
$privacyAttribute = $this->privacy->first(function($model) use ($attribute){
return $model->key == $attribute; // key being the column in the privacy model
});
return !is_null($privacyAttribute);
}
And then do your if statement this way:
if ($user->isPrivate('email') or $user->isPrivate('phone'))
Or a different implementation (usage is the same)
private $privacyAttributes = null;
public function isPrivate($attribute){
if($this->privacyAttributes == null){
$this->privacyAttributes = $this->privacy()->lists('key');
}
return in_array($attribute, $this->privacyAttributes);
}

User Model header:
/**
* Class User
* #package Interallmas
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
/**
* #var null|array
*/
protected $privacy_keys = NULL;
Privacy Relationship:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function privacy() {
return $this->hasMany('Interallmas\Privacy');
}
Privacy functions:
/**
* #return bool
*/
public function privacy_initialized() {
return ($this->privacy_keys !== NULL);
}
/**
* #return void
*/
public function initialize_privacy() {
if (!$this->privacy_initialized()) {
$this->privacy_keys = [];
foreach ($this->privacy as $privacy) {
$this->privacy_keys[] = $privacy->privacy_key;
}
}
}
/**
* #param $key
* #return bool
*/
public function isPrivate($key) {
$this->initialize_privacy();
return (in_array($key,$this->privacy_keys));
}
So: Whenever i access the isPrivate($key) method, i cache the result for the next use so i don't hit the server too hard - the function may be accessed once or more - i just query once, the first time. I believe for my needs, this is the best way to do it.

I think a simple count > 0 check should suffice. This requires you to have defined the relationship with the hasMany method for the User Model.
if (count($user->privacy) > 0) {
...
}

Related

Laravel factories association only associates the last data. How to fix this?

I have 2 models that needs to be associated once the data was created. I already asked it here and got the answer. I implemented it and made some modification because it doesn't associate the data correctly. Now I have problem with the association. It is only associated on the last data.
What I'm expecting is all the data created from the factory is associated to each other.
When I do php artisan tinker and run App\App::first()->load('menus', 'menus.page') command, it does not give me the correct data. Please take a look at the result here.
As you can see, the right data shown only on the last object. The rest is null. How can I fix this? Please take a look at my seeder file below, do I made any mistake?
<?php
use App\App;
use App\Component;
use App\Menu;
use App\Page;
use App\Role;
use App\Submenu;
use App\User;
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
$superadmin = Role::whereName('superadmin')->first();
$tester = new User();
$tester->name = "Tester";
$tester->email = "test#appbuilder.com";
$tester->password = bcrypt('password');
$tester->save();
$tester->roles()->attach($superadmin);
// Create 5 apps for each user
$tester->apps()->saveMany(factory(App::class, 5)->make())->each(function ($app) {
$menus = factory(Menu::class, 5)->make();
$pages = factory(Page::class, 5)->make();
// Create 5 menus for each app and 5 submenus for each menu
$app->menus()->saveMany($menus)->each(function ($menu) use ($pages) {
// Associate page with each menu
$pages->each(function ($page) use ($menu) {
$page->menu()->associate($menu);
});
// Create 5 submenus for each menu
$menu->submenus()->saveMany(factory(Submenu::class, 5)->make())->each(function ($submenu) use ($pages) {
// Associate page with each submenu
$pages->each(function ($page) use ($submenu) {
$page->submenu()->associate($submenu);
});
});
});
// Create 5 pages for each app and 5 components for each page
$app->pages()->saveMany($pages)->each(function ($page) {
$page->components()->saveMany(factory(Component::class, 5)->make());
});
});
}
}
Update
Here is my model files, please take a look:
App.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class App extends Model
{
/**
* The attributes that should not be mass assignable.
*
* #var array
*/
protected $guarded = ['id'];
/**
* Each app belongs to a user.
*
* #return \Illuminate\Database\Eloquent\Relationship\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Each app has many pages.
*
* #return \Illuminate\Database\Eloquent\Relationship\HasMany
*/
public function pages()
{
return $this->hasMany(Page::class);
}
/**
* Each app has many menus.
*
* #return \Illuminate\Database\Eloquent\Relationship\HasMany
*/
public function menus()
{
return $this->hasMany(Menu::class);
}
}
Menu.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Menu extends Model
{
/**
* The attributes that should not be mass assignable.
*
* #var array
*/
protected $guarded = ['id'];
/**
* Each menu belongs to a app.
*
* #return \Illuminate\Database\Eloquent\Relationship\BelongsTo
*/
public function app()
{
return $this->belongsTo(App::class);
}
/**
* Each menu has one page.
*
* #return \Illuminate\Database\Eloquent\Relationship\HasOne
*/
public function page()
{
return $this->hasOne(Page::class);
}
/**
* Each menu has many submenus.
*
* #return \Illuminate\Database\Eloquent\Relationship\HasMany
*/
public function submenus()
{
return $this->hasMany(Submenu::class);
}
/**
* Each menu belongs to many roles.
*
* #return \Illuminate\Database\Eloquent\Relationship\BelongsToMany
*/
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
Page.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
/**
* The attributes that should not be mass assignable.
*
* #var array
*/
protected $guarded = ['id'];
/**
* Each page belongs to an app.
*
* #return \Illuminate\Database\Eloquent\Relationship\BelongsTo
*/
public function app()
{
return $this->belongsTo(App::class);
}
/**
* Each page has many components.
*
* #return \Illuminate\Database\Eloquent\Relationship\HasMany
*/
public function components()
{
return $this->hasMany(Component::class);
}
/**
* Each page belongs to a menu.
*
* #return \Illuminate\Database\Eloquent\Relationship\BelongsTo
*/
public function menu()
{
return $this->belongsTo(Menu::class);
}
/**
* Each page belongs to a submenu.
*
* #return \Illuminate\Database\Eloquent\Relationship\BelongsTo
*/
public function submenu()
{
return $this->belongsTo(Submenu::class);
}
}
Well in fact you DID make a mistake. By looping through every page inside the menu loop. So in the first run every page would be associated the the first menu entry. In the second run every page with the second menu entry and so on. With finally leading to the fact, that every page is associated with the last menu entry.
To keep the structure like you have one could do something like this:
$tester = new \App\User();
$tester->name = "Tester";
$tester->email = "test#appbuilder.com";
$tester->password = bcrypt('password');
$tester->save();
// Create 5 apps for each user
$tester->apps()->saveMany(factory(\App\App::class, 5)->make())->each(function ($app) {
$menus = factory(\App\Menu::class, 5)->make();
$pages = factory(\App\Page::class, 5)->make();
$pagesIterator = $pages->getIterator();
// Create 5 menus for each app and 5 submenus for each menu
$app->menus()->saveMany($menus)->each(function ($menu) use ($pagesIterator) {
$page = current($pagesIterator);
$page->menu()->associate($menu);
// Create 5 submenus for each menu
$menu->submenus()->saveMany(factory(\App\Submenu::class, 5)->make())->each(function ($submenu) use ($page) {
// Associate page with each submenu
$page->submenu()->associate($submenu);
});
next($pagesIterator);
});
// Create 5 pages for each app and 5 components for each page
$app->pages()->saveMany($pages)->each(function ($page) {
$page->components()->saveMany(factory(\App\Component::class, 5)->make());
});
});
So we just get the next page from the collection inside the loop via an iterator and associate each menu with the next page from the collection. (Note that you would need to create more pages, if you want every submenu have a different page associated...)
Off topic:
$page->menu()->associate($menu);
normally needs a $page->save() followed by, as the associate() method does not save automatically. In your case the $pages get persisted to database by calling $app->pages()->saveMany($pages) at the end.
Are you willing to share your source code for the models? I think that would be helpful, coz I'm not quite sure what relations is $tester->apps() returning, is it belongsToMany or hasMany etc...
and reading saveMany's source code in Illuminate\Database\Eloquent\Relations\BelongsToMany:
/**
* Save an array of new models and attach them to the parent model.
*
* #param \Illuminate\Support\Collection|array $models
* #param array $pivotAttributes
* #return array
*/
public function saveMany($models, array $pivotAttributes = [])
{
foreach ($models as $key => $model) {
$this->save($model, (array) ($pivotAttributes[$key] ?? []), false);
}
$this->touchIfTouching();
return $models;
}
not sure if your getting an array or a collection, so this might be a case of a loop that wasn't executed, might as well do a foreach loop on saveMany's return value ie:
$savedModels = $tester->apps()->saveMany(factory(App::class, 5)->make());
// You can be sure that $model is an instance of `Model`
foreach($saveModels as $model){
// Do the logic here instead
}
anyway I can't dig deeper with the current info I have.

Doesn't it hurt Demeter's law when using services/factories in model?

class ForumThread
{
/**
* #return bool
*/
public function findBadLanguage ($inWhat)
{
return (bool)rand(0,1);
}
/**
* #return
*/
public function add ($threadName)
{
if (!$this->findBadLanguage ($threadName))
{
INSERT INTO
}
}
}
class ForumPost
{
/**
* #return
*/
public function post ($toThreadId, $comment)
{
// im talking about this:
Services::getForumThread()->findBadLanguage($comment);
}
}
I know findBadLanguage() should be in another class, but lets suppose thats okay. Lets focus on Services::get****() calls. Is it OK to turn to a global container and get objects from it? Or to turn to a factory? Doesnt it hury Demeter's law? It says we must not use object from the outside

Paris ORM, has_many_through with restrictions

What's the best way to approach this in Paris ORM?
I have a set of categories and a set of supplier profiles that havw a column called reatured. Currently my class is as follows:
<?php
namespace {
/**
* Class Category
*/
class Category extends ConfettiModel
{
public static $_table = 'supplier_directory_category';
/**
* Returns only top level categories - they have no parent
*
* #return bool
*/
public static function topLevel()
{
return self::where('parent', 0);
}
public static function marketing()
{
return self::where('marketing', 'Yes');
}
public function getTable() {
return self::$_table;
}
/**
* Is this a top level category - has no parent
*
* #return bool
*/
public function isTopLevel()
{
return ($this->parentId == 0);
}
/**
* Associated DirectoryProfile's
*
* #return ORMWrapper
*/
public function profiles()
{
return $this->has_many_through('DirectoryProfile', 'CategoryDirectoryProfile', 'category', 'supplier');
}
}
I'd like to add a new function, featuredProfiles() that allows me to retrieve the same results as profiles(), but in this case I want to restrict it to suppliers with featured = 'Yes'.
I'm not quite sure how to make this happen.
I took a punt and the answer was easier than I anticipated:
public function featuredProfiles() {
return $this->profiles()->where('featured', 'Yes');
}
The where is added as part of the query on the joined table.

Extend Entity with custom method to filter relations in Symfony2

I'm looking for a way to extend my Symfony2 (i currently use 2.3) Entity class with a method to effectively filter its relations on demand. So, imaging i have such 2 classes with OneToMany relation:
/**
* ME\MyBundle\Entity\Kindergarten
*/
class Kindergarten
{
/**
* #var integer $id
*/
private $id;
/**
* #var ME\MyBundle\Entity\Kinder
*/
private $kinders;
public function __construct()
{
$this->kinders = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get kinders
*
* #return Doctrine\Common\Collections\Collection
*/
public function getKinders()
{
return $this->kinders;
}
}
/**
* ME\MyBundle\Entity\Kinder
*/
class Kinder
{
/**
* #var integer $id
*/
private $id;
/**
* #var string $name
*/
private $name;
/**
* #var integer $age
*/
private $age;
}
My goal is to have a method on Kindergarten class to get on demand all kinders with age, for instance, between 10 and 12:
$myKindergarten->getKindersByAgeInInterval(10,12);
Of course, i can do something like:
class Kindergarten
{
...
public function getKindersByAgeInInterval($start, $end)
{
return $this->getKinders()->filter(
function($kinder) use ($start, $end)
{
$kinderAge = $kinder->getAge();
if($kinderAge < $start || $kinderAge > $end)
{
return false;
}
return true;
}
);
}
...
}
The solution above will work, but it's very inefficient, since I need to iterate across ALL kinders which can be a really big list and have no way to cache such filters. I have in mind usage of Criteria class or some proxy patterns, but not sure about a way to do it nice in Symfony2 especially since they probably will need access to EntityManager.
Any ideas?
I would suggest extracting this responsibility into an EntityRepository:
<?php
class KinderRepository extends \Doctrine\ORM\EntityRepository
{
public function findByKindergartenAndAge(Kindergarten $entity, $minAge = 10, $maxAge = 20)
{
return $this->createQueryBuilder()
->... // your query logic here
}
}
All the lookups should really happen in classes where you have access to the entity manager.
This is actually the way suggested by the Doctrine architecture. You can never have access to any services from your entities, and if you ever think you need it, well, then something is wrong with your architecture.
Of course, it may occur to you that the repository method could become pretty ugly if you later decide on adding more criteria (imagine you'll be searching by kindergarten, age, weight and height too, see http://www.whitewashing.de/2013/03/04/doctrine_repositories.html). Then you should consider implementing more logic, but again, that should not be that necessary.

How to implement Repository Pattern in Codeigniter?

When I programmed in ASP.NET MVC, there was a neat pattern called Repository. I want to implment it in Codeigniter but I do not know how. Here is what I actually want:
$mock_repository = new MockRepository();
$mock_repository->add(new Item(‘title1′, ‘description1′, 1));
$mock_repository->add(new Item(‘title2′, ‘description2′, 2));
$mock_repository->add(new Item(‘title3′, ‘description3′, 1));
$controller = new Item_controller($mock_repository);
$items = $controller->get_items_by_user_id(1);
$this->_assert_equals(count($items), 2);
I am using TOAST for Unit Testing. So how do I instantiate a controller within a test? The test is of course, another controller itself.
From what I know, to create a Generic Repository Pattern like in C#, you need 2 things PHP 5.6 dosen't have:
Real Method Overloading.
Generic Interface or Generic Abstract Class in PHP.
Click here for more on Generic Repository Pattern in C#.
However you can still create pseudo method overloading in PHP with the help of magic method __call, and we can type little more code for the generic part of the pattern.
Note: Before creating this pattern in Codeigniter 3.0 you will need to create a table in the database, and create auto loader for folder application/libraries.
First we need to create Interface in application/libraries folder:
<?php
interface IRepository
{
public function getById($id);
public function select($columns);
public function delete($id);
}
Seconde we need to create Abstract Class implementing the Interface and extending the CI_Model to be able to use the Database librarie:
<?php
abstract class Base_repository extends CI_Model implements IRepository
{
/**
* This must be valid table name in the Database.
*
* #var string $table Name of the table.
*/
protected $table;
public function __construct()
{
parent::__construct();
}
/**
* Pseudo method overloading.
* It's called when method is not declared in the abstract class.
*
* #param string $name Name of the method
* #param mixed $arguments Arguments of the method
*/
public function __call($name, $arguments)
{
switch ($name)
{
case 'save':
if ($arguments[0]->id > 0)
{
$this->update($arguments[0]);
}
else
{
$this->insert($arguments[0]);
}
break;
}
}
/**
* Get row with id.
*
* #param integer $id
* #return mixed
*/
public function getById($id)
{
return $this->db->get_where($this->table, ['id' => $id])->row_array();
}
/**
* Select columns.
*
* #param array $columns
* #return mixed
*/
public function select($columns = ['*'])
{
$this->db->select($columns);
return $this->db->get($this->table)->result();
}
/**
* Insert data.
*
* #param object $item
* #return void
*/
private function insert($item)
{
unset($item->id);
$this->db->insert($this->table, $item);
}
/**
* Update data.
*
* #param object $item
* #return void
*/
private function update($item)
{
$this->db->where('id =', $item->id);
unset($item->id);
$this->db->update($this->table, $item);
}
/**
* Delete data.
*
* #param integer $id
* #return void
*/
public function delete($id)
{
$this->db->delete($this->table, ['id' => $id]);
}
}
Third test the repository. Make a new model in application/model, and extend Base_repository, set table name and overload save method, create entity for this model:
<?php
/**
* The entity class.
*/
class Test
{
public $id;
public $info;
}
class Test_model extends Base_repository
{
/**
* Tell what table we are using.
*/
public function __construct()
{
parent::__construct();
$this->table = 'test';
}
/**
* "Overload" save method and call it from the parent.
*
* #param test $item Make use of the Dependency Injection.
* #return void
*/
public function save(Test $item)
{
parent::save($item);
}
}
Try it in the controller. Load the model and try to get, insert, ect...
To create real models is the same procedure. If you need to add more methods that will be the same for every model add them in the abstract class if you need to create methods only for specific model add it only in this model.
I don't recommend Codeigniter freamwork. Here are some patterns for PHP CLICK!
You would have to completely hijack the system files to load a controller from another controller. It can't be done, methinks.
It can be done with HMVC.
$result = Modules::run('controller/get_items_by_user_id', $params);
$this->_assert_equals($result, $expected);

Categories