I'm learning PHP and the Laravel framework and i'm getting stuck on loading some data from the database.
I have 2 models, Game and Player
Player model (curently empty):
class Player extends Model
{
}
Game model:
class Game extends Model
{
public $Players = [];
public function __construct(array $players = [])
{
$this->Players = $players;
}
public function SaveGame()
{
if($this->save()) {
Log::info("New game created with id " . $this->id);
}
foreach ($this->Players as $key => $player){
if ($this->Players()->save($player)){
Log::info("player added to game with id " . $player->id);
}
}
}
static function GetGame($gameId)
{
Return Game::find($gameId);
}
public function Players()
{
return $this->hasMany(Player::class );
}
}
And i have a GameController:
class GameController extends Controller
{
public function create()
{
$players = [];
for ($i = 1; $i <= 3; $i++) {
$player = new Player();
array_push($players,$player);
}
$game = new Game($players);
$game->SaveGame();
return $game;
}
public function LoadGame($id)
{
$game = Game::GetGame($id);
return $game;
}
}
If i call the create() method of my GameController, it creates a game in my Game table and 3 players in the player table with the gameID of the corresponding game so i think the relation works fine.
But if i check the response of the create() method i get only this:
{
"updated_at": "2018-03-01 15:13:37",
"created_at": "2018-03-01 15:13:37",
"id": 3952
}
Also the LoadGame() function returns just the game. Actually i need the game with all its players inside. Something like this:
{
"updated_at": "2018-03-01 15:13:37",
"created_at": "2018-03-01 15:13:37",
"id": 3952,
"players":[
{"name":"player1"},
{"name":"player2"},
{"name":"player3"},
],
}
What i'am doing wrong?/How do i get the wished result?
I'm also not sure if the Models are the right place to do the database actions and the controllers are the place to create the objects like i do in the posted code. If my "code structure" is not the best practice, some advice in this is also welcome!
To return the relationship you can use:
protected $appends = ['Players']
But to be honest, I think you may want to re-review the Laravel relationships docs to see how to do some of these things properly. Doing a bit of rewriting would make your life a bit easier in the future.
A couple notes for you:
Remove the GetGame method as it's arbitrary. By the time you've already pulled in the class App\Game you will be able to run the find method on it. You can just go: App\Game::find($id)
Remove the public $Players, as well as the construct, as these are unnecessary and breaks the relationship. Laravel makes it so that when you've created a relationship you can access it as if it's a variable within that class. Example: $game->players. You can kind of ignore the fact that the relationship is created as a method.
After doing those two things above, you can modify the protected $appends; variable and view your relationship when displaying the model.
Its so easy!, i found the solution by simply experimenting arround as i could not find a solution on the internet.
In the Game model I removed the
constructor (create own constructor in Laravel seems to be not recommended)
SaveGame() (save my game in the controller now)
GetGame() (useless, can use Game::find(x) in controller)
In the GameController i changed my create() function a bit. This is the result:
public function create()
{
$game = new Game(); // creates a new game
$game->Save(); // saves the new game into the db
for ($i = 1; $i <= 3; $i++) {
$player = new Player(); // creates a new player
$game->Players()->Save($player); // stores the player to the game in the db
}
}
I also changed my LoadGame() method in my GameController to
public function LoadGame($id)
{
$game = Game::find($id); // Instead of doing this in a own method in my model
$game->players // adds the players to the game object for some reason
return $game; // returns the game (as json) incl. all its players
}
This results in a json string what contains the game vars including a player array with its players. I dont understand why this is working but it is.
The weird thing is if i exchange $game->players for $game->Players() or $game->Players (like the has many method in the model) it does not work. Maybe someone can explain me why this is? (as i cant find any clarifying documentation to this)
Related
I am just doing my first exercises with Illuminate (Laravel) on my currently handmade database and website. I would like to improve by using MVC and found Illuminate interesting to use for the interaction with my database.
I worked the day on this bloody tiny code and can't find the problem and I hope someone out there has a good idea for me. Many thanks!
Basic question: Why can't I iterate over the courses to the given semester? While it is possible to call a specific course.
use Illuminate\Database\Eloquent\Model as Eloquent;
class Semester extends Eloquent {
protected $table = "tblSemester";
protected $primaryKey = "SemesterID";
public function getCourses() {
// test if semester is correct
echo $this->SemesterID, " - ", $this->Semestertext, "<br>";
// This works fine and returns the expected result
$course = $this->hasMany('Course', 'Semester')->first();
echo $course->Number, " - ", $course->Title;
// This doesn't work. It returns nothing.
// There seems to be no data in $courses (just metadata)
$courses = $this->hasMany('Course', 'Semester');
foreach ($courses as $course) {
echo $course->Number, " - ", $course->Title;
echo "<br>";
}
return "<h1>" . "Test ends" . "</h1>";
}
}
Many thanks!
Tim
First of all
Since you said you are new to 'Laravel' and Want to follow MVC, I thought I may give you some advices.
In laravel Models, don't include database manipulation codes. Instead write them in controllers.
In Models, only include functions which belongs to a single models instance (non static functions).
relationships
query scopes
accessors / modifiers
Example Model
class Semester extends Model
{
protected $table = 'semesters';
// relationships
public function cources()
{
return $this->hasMany(Cource::class);
}
}
Example Controller
class SemesterController extends Controller
{
public function iterateOverCourcesOfGivenSemester()
{
// take first semester as an example.
$givenSemester = Semester::first();
// all the cources belongs to a given semester.
$cources = $givenSemester->cources;
foreach($cources as $cource) {
// Warning! don't echo anything in a controller, Instead return them to a view.
echo $course->Number, " - ", $course->Title;
echo "<br>";
}
}
}
Why is this not working:
public function cources()
{
return $this->hasMany(Cource::class);
}
But this does:
public function cources()
{
return $this->hasMany('Course', 'Semester')->get();
}
I am currently developing an application for an indy movie production company. The way I have the workflow right now, the user begins by creating a new movie object by entering the movie title and synopsis. From there the user can then add more details such as price, run-time, full-screen/wide-screen, etc. The movie basic (title, synopsis) are in one database table, and the details are in another. I have set up a one-to-one relationship between the two eloquent models. I have also set up a MovieController that allows me to very easily do CRUD operations on the movie basic model, and when I am displaying the movie object to the user, I can display both the basics and details.
What I was wondering was there some way to use the already existent functions in the movie controller to do CRUD operations on the movie details without having to create new functions in the controller? Also is it possible to reuse the views I've created for each corresponding CRUD operation? In other words can I would like
something.dev/cms/create
In one instance to match to creating a new movie (title, synopsis) and in another instance to match to creating the movie detail (price, run-time, full-screen/widescreen) etc. Is this possible? I have provide the code for the two models below:
Movie_basic.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_basic extends Model {
protected $fillable = ['movie_title', 'movie_synopsis'];
protected $guarded = ['id'];
public function details()
{
return $this->hasOne('App\Movie_detail', 'movie_id');
}
public function personnel()
{
return $this->hasMany('App\Movie_personnel', 'movie_id');
}
}
Model_detail.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_detail extends Model {
protected $fillable = ['minutes', 'languages', 'viewer_discretion', 'screen_type', 'price'];
protected $guarded = ['id', 'movie_id'];
public function basics()
{
return $this->belongsTo('App\Movie_basic');
}
}
If I understand you, this might be an answer. (Did not test the code.)
Please note that, that code has been written to show you an example. You will probably want to edit it to make it work and act as you wanted. Maybe you want to use a repository or automate the model instance creating (I did not create new instances), and saving processes. You can use interfaces instead of your models etc...
Here is the service to store the logic.
<?php
use Movie_basic; use Movie_detail;
Class MovieService {
protected $movieBasic;
protected $movieDetail;
public function __construct(Movie_basic $movieBasic, Movie_detail $movieDetail) {
$this->movieBasic = $movieBasic;
$this->movieDetail = $movieDetail;
}
public function createMovie(array $attr) {
// TODO: Move your business logic here.
// E.g
$movie = $this->movieBasic->fill($attr);
$movie->save();
return $movie;
}
public function createMovieDetail(array $movieAttr, array $attributes) {
// TODO: Move your detail logic here.
// E.g.
$basic = $this->createMovie($movieAttr);
$detail = $this->movieDetail->fill($attributes);
$detail->basic()->associate($detail);
$detail->save();
return $detail;
}
}
And here, the controller examples:
<?php
use MovieService;
class MovieController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovie($attrToSave);
}
}
<?php
use MovieService;
class MovieDetailController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovieDetail($attrToSave);
}
}
We have two models
class Base extends AbstractModel
{
public $id;
public $name;
public function tableName()
{
return 'table_base';
}
}
class ExtendBase extends AbstractModel
{
public $id;
public $baseID;
public function tableName()
{
return 'table_base_extend';
}
}
I won't describe all, will describe only main things.
So, now I want create form that will combine this forms. And want in action do something like this
public function actionSome ()
{
$oBase = new Base();
$aExtendBase = array(); // important this is array
for ($i = 1; $i <= 3; $i++) {
$aExtendBase[] = new Extend(); // fill in array, pre-create for form
}
if ($this->isPost()) { // protected custom method
if (isset($_POST['Base'], $_POST['ExtendBase'])) // here should be specific checker because we have several ExtendBase instances in form
{
// save Base model
// save each ExtendBase model
}
}
$this->render('formView', array('oBase' => $oBase, 'aExtendBase' => $aExtendBaes))
}
So question is
How to create such 'combined' form in view;
How to get all forms data (I mean each) after POST action;
See the solution on the Yii WIKI
http://www.yiiframework.com/wiki/19/how-to-use-a-single-form-to-collect-data-for-two-or-more-models/
You can create a model (CFormModel) which does not represent an ActiveRecord just for a single form.
I think it would be the best way for you to proceed (assuming I've understood your problem correctly).
If I were you, I would create a Model to represent the rules for this form only (like I said, an instance of CFormModel, not ActiveRecord) and then, in your controller, take the elements that got submitted and manually create and/or update objects for both classes.
I have a simple database setup: Users, Groups, Pages - each are many to many.
See diagram: http://i.imgur.com/oFVsniH.png
Now I have a variable user id ($id), and with this I want to get back a list of the pages the user has access to, distinctly, since it's many-to-many on all tables.
I've setup my main models like so:
class User extends Eloquent {
protected $table = 'ssms_users';
public function groups()
{
return $this->belongsToMany('Group', 'ssms_groups_users', 'user_id','group_id');
}
}
class Group extends Eloquent {
protected $table = 'ssms_groups';
public function users()
{
return $this->belongsToMany('User', 'ssms_groups_users', 'user_id','group_id');
}
public function pages()
{
return $this->belongsToMany('Page', 'ssms_groups_pages', 'group_id','page_id');
}
}
class Page extends Eloquent {
protected $table = 'ssms_pages';
public function groups()
{
return $this->belongsToMany('Group', 'ssms_groups_pages', 'group_id','page_id');
}
}
I can get the groups the user belongs to by simply doing:
User::with('groups')->first(); // just the first user for now
However I'm totally lost on how to get the pages the user has access to (distinctly) with one query?
I believe the SQL would be something like:
select DISTINCT GP.page_id
from GroupUser GU
join GroupPage GP on GU.group_id = GP.group_id
where GU.user_id = $id
Can anyone help?
Thanks
TL;DR:
The fetchAll method below, in the MyCollection class, does the work. Simply call fetchAll($user->groups, 'pages');
Ok, assuming you managed to load the data (which should be done by eager-loading it, as mentioned in the other answer), you should loop through the Groups the User has, then loop through its Pages and add it to a new collection. Since I've had this problem already, I figured it would be easier to simply extend Laravel's own Collection class and add a generic method to do that.
To keep it simple, simply create a app/libraries folder and add it to your composer.json, under autoload -> classmap, which will take care of loading the class for us. Then put your extended Collection class in the folder.
app/libraries/MyCollection.php
use Illuminate\Database\Eloquent\Collection as IlluminateCollection;
class MyCollection extends IlluminateCollection {
public function fetchAll($allProps, &$newCollection = null) {
$allProps = explode('.', $allProps);
$curProp = array_shift($allProps);
// If this is the initial call, $newCollection should most likely be
// null and we'll have to instantiate it here
if ($newCollection === null) {
$newCollection = new self();
}
if (count($allProps) === 0) {
// If this is the last property we want, then do gather it, checking
// for duplicates using the model's key
foreach ($this as $item) {
foreach ($item->$curProp as $prop) {
if (! $newCollection->contains($prop->getKey())) {
$newCollection->push($prop);
}
}
}
} else {
// If we do have nested properties to gather, then pass we do it
// recursively, passing the $newCollection object by reference
foreach ($this as $item) {
foreach ($item->$curProp as $prop) {
static::make($prop)->fetchAll(implode('.', $allProps), $newCollection);
}
}
}
return $newCollection;
}
}
But then, to make sure your models will be using this class, and not the original Illuminate\Database\Eloquent\Collection, you'll have to create a base model from which you'll extend all your models, and overwrite the newCollection method.
app/models/BaseModel.php
abstract class BaseModel extends Eloquent {
public function newCollection(array $models = array()) {
return new MyCollection($models);
}
}
Don't forget that your models should now extend BaseModel, instead of Eloquent. After all that is done, to get all your User's Pages, having only its ID, do:
$user = User::with(array('groups', 'groups.pages'))
->find($id);
$pages = $user->groups->fetchAll('pages');
Have you tried something like this before?
$pages = User::with(array('groups', 'groups.pages'))->get();
Eager loading might be the solution to your problem: eager loading
I'm trying to figure out how to reuse Domain Models in different parts of the application and I have a feeling that the Data Mapper pattern is the way forward. The example below has methods that directly access the methods of the Mapper.
class Groups
{
protected $_groups = array();
public function addGroup($name)
{
$this->_groups[] = $name;
}
public function doSomethingGroupy($cakes)
{
// get all the groups that have cake
return $cakeyGroups;
}
}
... And a mapper to match the methods on the Groups class.
class GroupMapper
{
public function find($id, Groups $group)
{
// Mappy type things, maybe some sql
}
public function fetchByNeediness($cuddles, Groups $group)
{
// More mappy type things
}
public function save(Groups $groups)
{
// Saves
}
}
However if sometime later I wanted to use the same Groups Models but populate the groups using different queries I would use a different mapper.
class AngryGroupMapper
{
public function find($id, Groups $group)
{
// Something similar but with other tables and joins
}
public function fetchByRage($anger, Groups $group)
{
// Something new but only needed here
}
public function isEditable(Groups $groups)
{
// Do some querying
return $bool;
{
}
Now I Know the aim is Skinny Controller - Fat Model, so would I have another model to Map the Mapper (so to speak) to the Model?
class FatModelRepository
{
public function getHappyGroups()
{
$mapper = new GroupMapper();
return $mapper->fetchByNeediness('Puffy Shoes', new Groups());
}
public function getSadGroups()
{
$mapper = new AngryGroupMapper();
return $mapper->fetchByRage('Aghh!', new Groups());
{
public function save(Groups $groups)
{
$mapper = new GroupMapper();
return $mapper->save($groups);
{
}
The Data Model should have no knowledge of the Data Mapper. Your Groups class/model shouldn't have find methods and it should not have access to the mapper.
Once you remove the mapper dependency from your model your problems will go away.
NOTE: check out Doctrine 2
As rojoca says you shouldnt have the fetch/find methods directly on the model. Technically hes also right about the model not storing a reference to the mapper, but in less complex situations i think this is ok so long as the model only excpets the most abstract form of mapper you plan on having (ie. some kind of base mapper class or an interface).
Given thos to things, you should only need to add methods to the mapper, and for this i would just use inheritance, ie. extend your groups mapper for the new functionality. Of course this requires that the mapper is injectable into the model. But if youre going to have the model hold a reference to its mapper then it does need to be injectable anyhow.