I am trying to assign a value to $table property in my Model, based on the URL string. There I am getting "Array to string conversion" error. Below are code level details. Can someone please help? Thanks.
I have coded my model __constructor() as shown below.
<?php
use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;
class Disaster extends Eloquent {
use UserTrait,
RemindableTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table;
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = array('password', 'remember_token');
public function __construct($disasterArea, $disaster = "flood") {
$this->table = $disasterArea . '_' . $disaster;
}
}
I am trying to pass required values while Model instantiation from my controller as shown below.
class DisasterController extends \BaseController {
public function getFloodTweets($floodArea){
$flood = new Disaster($floodArea, "floods");
$floodTweets = $flood->orderBy('created_at','desc')->groupBy('tweets')->paginate(10);
$floodAreaUc = ucfirst($floodArea);
return View::make("disaster.floodTweets",['title'=>"$floodAreaUc Flood",'floodTweets'=>$floodTweets,'floodArea'=>$floodAreaUc]);
}
}
}
that means if I trigger an URL like www.example.com/floods/city my model should build the table as 'city_floods' which is the naming convention we are following.
And also, I observed table name is being build correctly but throwing this error. And strange thing is my code works fine when I hard code this table name. i.e.
$this->table = "city_floods" works fine but
$this->table = $disasterArea . '_' . $disaster do not. I don't understand what is the difference. Can someone please suggest where I am doing wrong.
I working on UBUNTU 14.04 with Laravel 4.2 framework.
Edit
Well You cannot pass the data in that way to the eloquent model because the original abstract eloquent model have the first parameter as array! and it will try to put it in this way.
EDIT
As you can see on your screen shot - the model is resolving somewhere with a newInstance method what means that it trying to put (propably) an empty array into you string varible.
The solution
The best option is to split the logic into few Disaster Models that extends the main Disaster model and resolving them from factory like that:
class UsaFloodDisaster extends Disaster
{
protected $table = 'usa_flood';
}
class FranceFloodDisaster extends Disaster
{
protected $table = 'france_flood';
}
(...)
class DisasterFactory
{
public function make($area, $disaster = 'flood')
{
$className = ucfirst(camel_case($area.'_'.$disaster));
return app($className);
}
}
then your controller would looks like:
class DisasterController extends \BaseController {
public function getFloodTweets($floodArea){
$flood = app(DisasterFactory::class)->make($floodArea, "floods");
$floodTweets = $flood->orderBy('created_at','desc')->groupBy('tweets')->paginate(10);
$floodAreaUc = ucfirst($floodArea);
return View::make("disaster.floodTweets",['title'=>"$floodAreaUc Flood",'floodTweets'=>$floodTweets,'floodArea'=>$floodAreaUc]);
}
}
Related
I'm using Laravel 6 with a SQL Server 2017 database backend. In the database I have a table called PersonPhoto, with a Photo column and a Thumbnail column where the photos and thumbnails are stored as VARBINARY.
I have defined the following Eloquent model, with two Accessors to convert the images to base64 encoding:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PersonPhoto extends Model
{
protected $connection = 'mydb';
protected $table = 'PersonPhoto';
protected $primaryKey ='PersonID';
public function person(){
return $this->belongsTo('App\Person', 'PersonID');
}
public function getPhotoAttribute($value){
return base64_encode($value);
}
public function getThumbnailAttribute($value){
return base64_encode($value);
}
}
This works fine in Blade templates, however when I try to serialize to JSON or an Array I get a "Malformed UTF-8 characters, possibly incorrectly encoded" error, as if the Accessors are being ignored and the raw data is being serialized. To workaround this, I have altered the model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PersonPhoto extends Model
{
protected $connection = 'mydb';
protected $table = 'PersonPhoto';
protected $primaryKey ='PersonID';
//Added to hide from and add fields to serializer
protected $hidden = ['Photo', 'Thumbnail'];
protected $appends = ['encoded_photo', 'encoded_thumbnail'];
public function person(){
return $this->belongsTo('App\Person', 'PersonID');
}
public function getPhotoAttribute($value){
return base64_encode($value);
}
public function getThumbnailAttribute($value){
return base64_encode($value);
}
//Added these new accessors
public function getEncodedPhotoAttribute(){
return base64_encode($this->Photo);
}
public function getEncodedThumbnailAttribute(){
return base64_encode($this->Thumbnail);
}
}
This hides the original Photo and Thumbnail fields from the serializer and includes the two new accessors. This appears to work and solves my issue.
Questions:
1) Is Laravel's serializer ignoring my Accessors as I suspect, and is this by design?
2) Although my workaround works, is this a reasonable approach or am I likely to run into problems? Is there a better way of doing it?
Thanks
I think you have two issues:
First, Laravel serialization requires that you append any accessors you want included — even if an attribute of the same name already exists. You did not explicitly append the desired values in the first example.
https://laravel.com/docs/5.8/eloquent-serialization#appending-values-to-json
Second, Laravel doesn't always like capitalized attribute names. It happily expects everything to be lowercase (snake_case) and based on some quick testing, seems to have some trouble associating a proper $value to pass to an accessor when case is involved.
However, you can modify your accessor to call the attribute directly instead of relying on Laravel to figure out what you are asking for and achieve the desired results.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PersonPhoto extends Model
{
protected $connection = 'mydb';
protected $table = 'PersonPhoto';
protected $primaryKey = 'PersonID';
// add the desired appends for serialization
protected $appends = ['Photo','Thumbnail'];
public function person()
{
return $this->belongsTo('App\Person', 'PersonID');
}
public function getPhotoAttribute()
{
// access the attribute directly
return base64_encode($this->attributes['Photo']);
}
public function getThumbnailAttribute()
{
// access the attribute directly
return base64_encode($this->attributes['Thumbnail']);
}
}
EDIT: I actually see that you did something similar in your second example with $this->Thumbnail and $this->Photo. My example is of the same concept, but without relying on magic methods.
__get/__set/__call performance questions with PHP
I'm developing an application where my data comes from external server in JSON format.
I would like to set a relationships between each models, but without using a database table.
Is it possible ?
Something like that:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'https://.../server/flights.json';
}
You could make a service class which handles the request and returns class instances:
namespace App\Services;
class FlightService
{
/**
* #var FlightFactory
*/
private $flightFactory;
public function __construct(FlightFactory $flightFactory)
{
$this->flightFactory = $flightFactory;
}
public function getAllFlights()
{
$flightsJson = $this->getFromExternalCurl();
return $this->flightFactory->buildFlightList($flightsJson);
}
private function getFromExternalCurl()
{
return Curl::to('http://www.foo.com/flights.json')
->withData( array( 'foz' => 'baz' ) )
->asJson()
->get();
}
}
Basically the service would make the external API call and the response is passed to a factory which creates the instances.
Note that you just need to add the factory in the construct and it's binded because laravel uses https://laravel.com/docs/5.4/container
namespace App\Factories;
class FlightFactory
{
public function buildFlightList($flightJsonList)
{
$flightCollection = collect();
foreach($flightJsonList as $flightJson) {
$flightCollection->push($this->buildFlight($flightJson));
}
return $flightCollection;
}
public function buildFlight($flightJson)
{
$flight = new Flight();
// add properties
return $flight;
}
}
The factory will return a Collection which is verry usefull because it contains usefull methods, or you can return an array.
In this example I used a curl library https://github.com/ixudra/curl but it can be replaced with native php or other libraries.
Then you can use by injecting the FlightService in your controllers.
P.S: Code not tested but represents a possible approach
So for my project model setAppends([]) works as below:
Project::find($projectId)->setAppends([])
but what if I want to set appends to empty array for a relation which I'm eager loading with with, like below:
$project = Project::with('pages')->find($projectId);
->setAppends([]) not working in above code, as it will set it to empty array for Project not for Page.
Can anyone guide how to achieve that ?
Update:
page.php Model has appends and hidden like this:
class Page extends Model {
// I don't want to load this (`appends`) attributes when I call Project::find($projectId)
protected $appends = ['thumbnail_url', 'total_annotations', 'total_tasks', 'total_done_tasks', 'image_url', 'edited_data_items_count'];
protected $hidden = ['tasksCount', 'doneTasksCount', 'annotationsCount', 'xsl', 'xml', 'dataxml_version', 'sort_order', 'editedDataItemsCount', 'deletedDataItemsCount'];
}
Project.php model looks like this:
class Project extends Model {
use SoftDeletes;
protected $appends = ['total_tasks', 'total_done_tasks', 'total_pages', 'total_annotations', 'edited_dataitems_total_count'];
protected $hidden = ['tasksCount', 'doneTasksCount', 'pagesCount', 'annotationsCount', 'folder_path', 'attachment_url', 'pages'];
}
On Project you may provide a static method, which allows you to iterate over the eagerly loaded pages and adjust their append-array.
class Project
{
...
public static function eagerFindWithoutAppends($projectId)
{
$model = self::with('pages')->find($projectId);
$model->setAppends([]);
foreach ($model->pages as $page) {
$page->setAppends([]);
}
return $model;
}
...
}
But if I understand correctly, the dynamic data in your Pages class does more than just providing convenient shortcuts based on the regularly loaded data (such as something like getFullName which would combine first_name and last_name).
What do your appends do?
I don't want to load this (appends) attributes
Another possible solution I could think of is to inherit NoneAppendPages from Pages and override $append and all the related get... methods.
Then in Project declare another relationship to NoneAppendPages next to Pages. You then eager load Project::::with('none_append_pages')->find($projectId);
class NoneAppendPages extends Pages
{
protected $appends = [];
getYourDynamicAttributeMethodName() { return null; } // for all your appends
}
class Project
{
public function pages()
{
// I don't know what relationship you declared / assuming on to many
return $this->hasMany('App\Page');
}
public function noneAppendPages()
{
// declare the same way you did with pages
return $this->hasMany('App\NoneAppendPage');
}
}
The given solution does not work when using a package that does a lot of the work after you define the with() relations like datatables
here is a solution that works for any model.
<?php
namespace App\Database;
trait Appendable {
static protected $static_appends = [];
static protected $static_replace_appends = null;
/**
* set a static appends array to add to or replace the existing appends array..
* replace => totally replaces the existing models appends array at time of calling getArrayableAppends
* add => merges and then makes unique. when getArrayableAppends is called. also merges with the existing static_appends array
*
* #param $appendsArray
* #param bool $replaceExisting
*/
public static function setStaticAppends($appendsArray, $replaceExisting = true)
{
if($replaceExisting) {
static::$static_replace_appends = true;
static::$static_appends = array_unique($appendsArray);
} else {
static::$static_replace_appends = false;
static::$static_appends = array_unique(array_merge(static::$static_appends,$appendsArray));
}
}
/**
* Get all of the appendable values that are arrayable.
*
* #return array
*/
protected function getArrayableAppends()
{
if(!is_null(static::$static_replace_appends)) {
if(static::$static_replace_appends) {
$this->appends = array_unique(array_merge(static::$static_appends,$this->appends??[]));
} else {
$this->appends = static::$static_appends;
}
}
return parent::getArrayableAppends();
}
}
then you can just apply the trait to any model
<?php
namespace App\Database;
abstract class Company
{
use Appendable;
}
then call the static method BEFORE you use the relationship
<?php
$replaceCurrentAppendsArray = true;
// this will remove the original appends by replacing with empty array
\App\Database\Company::setStaticAppends([],$replaceCurrentAppendsArray);
$replaceCurrentAppendsArray = true;
// this will remove the original appends by replacing with smaller array
\App\Database\Company::setStaticAppends(['thumbnail_url'],$replaceCurrentAppendsArray);
$replaceCurrentAppendsArray = FALSE;
// this will add to the original appends by providing an additional array element
\App\Database\Company::setStaticAppends(['my_other_attribute'],$replaceCurrentAppendsArray);
this will allow you to override the appends array provided on the model even if another package is going to be loading the model. Like yajra/laravel-datatable where my issue was and brought me to this page which inspired a more dynamic solution.
This is similar to Stefan's second approach, but this is more dynamic so you do not have to create additional model extensions to accomplish the overrides.
You could take a similar approach to override the HidesAttribute trait as well.
I want to replace the Laravels builder class with my own that's extending from it. I thought it would be as simple as matter of App::bind but it seems that does not work. Where should I place the binding and what is the proper way to do that in Laravel?
This is what I have tried:
my Builder:
use Illuminate\Database\Eloquent\Builder as BaseBuilder;
class Builder extends BaseBuilder
{
/**
* Find a model by its primary key.
*
* #param mixed $id
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|static|null
*/
public function find($id, $columns = array('*'))
{
Event::fire('before.find', array($this));
$result = parent::find($id, $columns);
Event::fire('after.find', array($this));
return $result;
}
}
And next I tried to register the binding in bootstrap/start.php file like this :
$app->bind('Illuminate\\Database\\Eloquent\\Builder', 'MyNameSpace\\Database\\Eloquent\\Builder');
return $app;
Illuminate\Database\Eloquent\Builder class is an internal class and as such it is not dependency injected into the Illuminate\Database\Eloquent\Model class, but kind of hard coded there.
To do what you want to do, I would extend the Illuminate\Database\Eloquent\Model to MyNamespace\Database\Eloquent\Model class and override newEloquentBuilder function.
public function newEloquentBuilder($query)
{
return new MyNamespace\Database\Eloquent\Builder($query);
}
Then alias MyNamespace\Database\Eloquent\Model to Eloquent at the aliases in app/config/app.php
Both of the answers are correct in some way. You have to decide what your goal is.
Change Eloquent Builder
For example, if you want to add a new method only for eloquent models (eg. something like scopes, but maybe a little more advanced so it’s not possible in a scope)
Create a new Class extending the Eloquent Builder, for Example CustomEloquentBuilder.
use Illuminate\Database\Eloquent\Builder;
class CustomEloquentBuilder extends Builder
{
public function myMethod()
{
// some method things
}
}
Create a Custom Model and overwrite the method newEloquentBuilder
use Namespace\Of\CustomEloquentBuilder;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
public function newEloquentBuilder($query)
{
return new CustomEloquentBuilder($query);
}
}
Change Database Query Builder
For example to modify the where-clause for all database accesses
Create a new Class extending the Database Builder, for Example CustomQueryBuilder.
use Illuminate\Database\Query\Builder;
class CustomQueryBuilder extends Builder
{
public function myMethod()
{
// some method things
}
}
Create a Custom Model and overwrite the method newBaseQueryBuilder
use Namespace\Of\CustomQueryBuilder;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new CustomQueryBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
}
Laravel Version: 5.5 / this code is untestet
The answer above doesn't exactly work for laravel > 5 so I done some digging and I found this!
https://github.com/laravel/framework/blob/5.2/src/Illuminate/Database/Eloquent/Model.php#L1868
use this instead!
protected function newBaseQueryBuilder()
{
$conn = $this->getConnection();
$grammar = $conn->getQueryGrammar();
return new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
}
I'm trying to return an object Contract and all of it's related Project. I can return all of the Contracts but when I try to get the contract's Project, I get a "Class 'EstimateProject' not found" error. I've run composer dump-autoload to reload the class mappings, but I still get the error. Any ideas? Here's my class setup:
EDIT: Just wanted to add that LaravelBook\Ardent\Ardent\ is an extension of Laravel's Model.php. It adds validation to model on the Save function. I've made Ardent extend another plugin I've added that is a MongoDB version of the Eloquent ORM.
EstimateContract.php
<?php namespace Test\Tools;
use LaravelBook\Ardent\Ardent;
class EstimateContract extends Ardent {
// This sets the value on the Mongodb plugin's '$collection'
protected $collection = 'Contracts';
public function projects()
{
return $this->hasMany('EstimateProject', 'contractId');
}
}
EstimateProject.php
<?php namespace Test\Tools;
use LaravelBook\Ardent\Ardent;
class EstimateProject extends Ardent {
// This sets the value on the Mongodb plugin's '$collection'
protected $collection = 'Projects';
public function contract()
{
return $this->belongsTo('EstimateContract', 'contractId');
}
}
EstimateContractController.php
<?php
use \Test\Tools\EstimateContract;
class EstimateContractsController extends \BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
$contracts = EstimateContract::all();
echo $contracts;
foreach($contracts as $contract)
{
if($contract->projects)
{
echo $contract->projects;
}
}
}
}
In order for this to work, I needed to fully qualify the EstimateProject string in my EstimateContract model.
The solution was to change it from:
return $this->hasMany('EstimateProject', 'contractId');
to
return $this->hasMany('\Test\Tools\EstimateProject', 'contractId');
You have to use the fully qualified name, but I got the same error when I used forward slashes instead of back slashes:
//Correct
return $this->hasMany('Fully\Qualified\ClassName');
//Incorrect
return $this->hasMany('Fully/Qualified/ClassName');