简体   繁体   中英

Laravel object property is null when looping but has value when json_encode()

My View piece of code:

  @forelse ($questions as $question)
            <a href="#" class="list-group-item">
                <h4>{{$question->title}}</h4>
                <p>{{Str::limit($question->body, 100)}}</p>
                <span>Asked {{$question->created}} by {{$question->user_name}}</span>
                {{json_encode($question)}}
            </a>
        @empty
            <h2>No questions!</h2>
        @endforelse

I'm getting html like this ('...' is ok, that's valid but value is long):

<a href="#" class="list-group-item">
            <h4>...</h4>
            <p>...</p>
            <span>Asked  by </span> 
            <br>
            {"id":1,"title":"...","path":"...","body":"...","created":"2 hours ago","user_name":"Nash Bogisich","user_id":11}
        </a>

As you see, inside html created is null , as well as user_name . But inside json, these props have theirs values. I don't understand how that's possible, please help.

QuestionResource is completely plain:

class QuestionResource extends JsonResource
{

    public function toArray($request)
    {
        return [
            'id' =>$this->id,
            'title' =>$this->title,
            'path' =>$this->path,
            'body' =>$this->body,
            'created' => $this->created_at->diffForHumans(),
//            'modified' =>$this->modified_at,
            'user_name' =>$this->user->name,
            'user_id' =>$this->user_id,
        ];
    }
}

I also checked that created is really null .

if ($question->created==null){
                        $created = 'null value';
                    }

Yes, null value.

$questions isn't really array (I think), it's a Laravel collection.

Controllers (I have two, one is for api and one is for web . Web uses api .):

API:

   public function index()
    {
        // now each transform of question use QuestionResource
        return QuestionResource::collection(Question::latest()->paginate(15));
    }

WEB:

    public function index()
    {
        $questions = $this->apiController->index();
//        return $questions;
        return View::make('question.index')->with('questions', $questions);
    }

First, let's stabilish this:

From PHP's Type Juggling : ...a variable's type is determined by the context in which the variable is used.

Simple example for insight:

$thisIsAnObject = new Object;
echo $thisIsAnObject;
// as echo expects a string, PHP will attempt to cast it to string and it
// will fail with a message similar to: "Object could not be converted to string"

However, Laravel, specially in Illuminate\\Support\\Collection , takes over of this automatic casting and feeds the context in the type it requires.

Simple example for insight:

$simpleCollection = collect([['key' => 'value'],['key' => 'value']]);
echo $simpleCollection; 
// instead of letting it error, laravel feeds the echo statement with the json
// representation of the collection (which is a string): [{"key":"value"},{"key":"value"}]
// check Illuminate\Support\Collection::__toString() function

Laravel does the exactly same for foreach :

$collection = Question::latest()->paginate(15);
foreach ($collection as $model) {}
// collection feeds the foreach an array of models for better manusing.
// check Illuminate\Support\Collection::toArray() function

To wrap it all up , notice that the QuestionResource 's mapping takes place inside a toArray() function.

So this:

$questionResource = QuestionResource::collection(Question::latest()->paginate(15));

if you feed a foreach with the $questionResource above, the framework intentions are to feed the foreach with an array of its collection models, ignoring your intention to cast everything to primitive array with QuestionResource 's ->toArray() .

However, for example, if you were to pass $questionResource to the json_encode() function, which expects a string and does not involves any looping, the framework has no problem resolving the QuestionResource 's ->toArray() first and then casting it to the json representation of the collection.

That's what's happening with your code above.

I will write 2 ways of getting by this issue, from fastest to finest:

1) calling ->toArray() to force the casting beforehand

$questions = $this->apiController->index()->toArray(request());
@forelse ($questions as $question)
    <a href="#" class="list-group-item">
        <h4>{{$question['title']}}</h4>
        <p>{{Str::limit($question['body'], 100)}}</p>
        <span>Asked {{$question['created']}} by {{$question['user_name']}}</span>
    </a>
@empty
    <h2>No questions!</h2>
@endforelse

2) create an accessor for created and user_name fields in your Question model

class Question extends Model
{
    // $this->created
    public function getCreatedAttribute()
    {
        return $this->created_at->diffForHumans();
    }

    // $this->user_name
    public function getUserNameAttribute()
    {
        return $this->user->name;
    }
}

Like so, you are persisting those fields, by having them available everywhere there's an instance of the Question model. Of course, that includes QuestionResource .

class QuestionResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'path' => $this->path,
            'body' => $this->body,
            'created' =>  $this->created,
            // 'modified' => $this->modified_at,
            'user_name' => $this->user_name,
            'user_id' => $this->user_id
        ];
    }
}
@forelse ($questions as $question)
    <a href="#" class="list-group-item">
        <h4>{{$question->title}}</h4>
        <p>{{Str::limit($question->body, 100)}}</p>
        <span>Asked {{$question->created}} by {{$question->user_name}}</span>
        {{json_encode($question)}}
    </a>
@empty
    <h2>No questions!</h2>
@endforelse

Hope it helps.

I think you are getting a bit confused by the API and Web responses.

First you define the QuestionResource API resource, that's fine and you put the created attribute correctly.

If you then look at your view file, you are iterating over a collection where each object is a QuestionResource object type.

To get the attribute you want you should print out {{ $question->created_at->diffForHumans() }} as you are still interacting magically with a Question model.

Quick in-depth explaination

When using API resources you should keep in mind that the PHP Object -> JSON string conversion happens as later stage after your controller have hadled the request and before the router componen send out the response to the user, if you call your method from another one, you still haven't been through the json transforming process as it is eventually part of a later stage in the core code.

This means that the $question return value you are getting from $this->apiController->index() call, would give you a collection of QuestionResource , not a json string of the response.

To understand how you can still access the Question model even if you have an array of QuestionResource objects, you have to take a look at the JsonResource class in the laravel core.

Every API resource class in laravel inherits from it. This class uses the DelegateToResource trait (this one is defined as well in Laravel's core) which defines this method:

/**
 * Dynamically get properties from the underlying resource.
 *
 * @param  string  $key
 * @return mixed
 */
public function __get($key)
{
    return $this->resource->{$key};
}

This means that when you have a $question object that is of type QuestionResource you can still access the underlying resource by simply "pretending" your resource object is a question, as it will act like so and would automatically forward all the method calls and attribute read to the Question model.

If you are questioning where you did tell the QuestionResource to wrap the Question model in itself, you did it in the return instruction of your index method:

return QuestionResource::collection(Question::latest()->paginate(15));

You can see this return instruction as a "simple" wrapping of each Question model you pass as an argument in a bigger object (the QuestionResource class) that adds a method to represent the Question as an array that will be therefore used to converted the model to json at a later stage (as I mentioned eariler).

So to retrive fields correctly in your @foreach loop you have to refer to your model attributes' names, not the ones you defined as a return for the toArray() method, as that would be executed at a later stage.

So, why does {{json_encode($question)}} output the fields defined in the resource?

Because JsonResource class implements the PHP's JsonSerializable interface, this means it implements the jsonSerialize method (defined by the interface) to force the json conversion.

When you call json_encode method, php will recognize that $question implements that interface and therefore is serializable, so it call the jsonSerialize method and get the desired string.

As a result you get the api resource output and converted to json, and that output have the fields you defined in the QuestionResource class

Thanks to @mdexp answer and @Welder Lourenço.

These solutions worked for me:

  1. Just call json_encode and json_decode in my web QuestionController . Very simple, do not produce much overhead. But it's a workaround.

  2. Specify property get-accessors in Question Model class. Subtle. And the Model class is the right place to define accessors. Now I do use that solution.

Code for #1:

public function index()
{
    $str = json_encode($this->apiController->index());
    $questions = json_decode($str);

    return View::make('question.index')->with('questions', $questions);
}

Code for #2:

// in Question model class...

// $this->created
public function getCreatedAttribute()
{
    return $this->created_at->diffForHumans();
}

// $this->user_name
public function getUserNameAttribute()
{
    return $this->user->name;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM