php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Request #74425 Behaviour Change: Non-static method should not be called statically
Submitted: 2017-04-13 01:28 UTC Modified: 2017-04-13 08:32 UTC
Votes:1
Avg. Score:5.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:1 (100.0%)
Same OS:1 (100.0%)
From: adrianrb93 at gmail dot com Assigned:
Status: Closed Package: Class/Object related
PHP Version: 7.0.17 OS: MACOS 10.12.14
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: adrianrb93 at gmail dot com
New email:
PHP Version: OS:

 

 [2017-04-13 01:28 UTC] adrianrb93 at gmail dot com
Description:
------------
I added to my class a non-static method intentionally expecting that if it is called statically, it will direct to the __callStatic() magic method.

Use Case:

I am using Laravel. On its model classes:

* Model: calling a non-existing static method is handled by `__callStatic()`, for this example lets pretend its `->admin()`
* Model: `callStatic` creates a query builder class and attempts to run the method on that
* Query Builder: The query builder does not have this method so it is handled by `__call()`
* Query Builder: `__call()` will try to call the method on the Model prepended with 'scope': `$this->getModel()->scopeAdmin($this, ...$parameters)`
* Model: The `scopeAdmin()` method exists

In my test script example, I will be using a `scopeOpened()` and an `opened()` method, both which are non-static methods.


Test script:
---------------
<?php

/**
 * ----------------------------------------------------------------------------
 * Instructions:
 * ----------------------------------------------------------------------------
 * Install a new Laravel 5.4 app and place this test script in: `app/Shop.php`
 *
 * In the Laravel project run `composer install`. The `php artisan tinker`
 * command will give you the same test environment to test the behaviour.
 */

/**
 * ----------------------------------------------------------------------------
 * Expected Result:
 * ----------------------------------------------------------------------------
 * $ php artisan tinker
 * Psy Shell v0.8.3 (PHP 7.0.17 — cli) by Justin Hileman
 * >>> App\Shop::opened('2017-04-13')
 * => App\Shop {#970
 *      owner_id: null,
 *      day_of_the_week: 4,
 *      start_date: "2017-04-13",
 *      end_date: "2017-04-13",
 *    }
 */

/**
 * ----------------------------------------------------------------------------
 * Actual Result:
 * ----------------------------------------------------------------------------
 * $ php artisan tinker
 * Psy Shell v0.8.3 (PHP 7.0.17 — cli) by Justin Hileman
 * >>> App\Shop::opened('2017-04-13')
 * ErrorException with message 'Non-static method App\Shop::opened() should not be called statically'
 */

namespace App;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model as BaseModel;

class Shop extends BaseModel
{
    /**
     * This is what is expected to be run when Shop::opened() is called.
     *
     * -  Shop::opened()
     * -> Model::__callStatic()
     * -> $builder->opened()
     * -> $builder->__call()
     * -> $model->scopeOpened()
     * -> $model->opened()
     *
     * The above will not work because within a static context, PHP attempts
     * to call the non-static method `opened()` instead of directing the call
     * to `__callStatic()`.
     *
     * @param  Illuminate\Database\Eloquent\Builder $query
     * @return $this    Usually you return the query builder, but my intent is
     *                  to have data pulled from the query builder, then
     *                  $model->opened() be called.
     */
    public function scopeOpened($query, $date)
    {
        return $query->getModel()->fill([
            // ----------------------------------------------------------------
            // This is an example, the query builder could have added
            // `where owner_id = '1'`. This scope method is here to direct to
            // the opened() method on the query builders model instance with
            // the `owner_id` provided if given in the query.
            //
            // This is to show I have an actual need for the behaviour I'm
            // requesting.
            // ----------------------------------------------------------------
            'owner_id' => self::extractWhereValueFromQuery($query, 'owner_id'),
        ])->opened($date);
    }

    /**
     * This is intended to only be called in a non-static context. The
     * behaviour I'm requesting is for PHP to see this isn't a static method
     * and instead direct to the magic __callStatic() method.
     *
     * @param  Carbon\Carbon|string $date
     * @return $this
     */
    public function opened($date)
    {
        $date = self::resolveDate($date);
        return $this->fill([
            'day_of_week' => $date->dayOfWeek,
            'start_date'  => $date->toDateString(),
            'finish_date' => $date->toDateString(),
        ]);
    }

    /**
     * Extract column value from query builders where clause.
     *
     * @param  Illuminate\Database\Eloquent\Builder $query
     * @param  string $column
     * @return mixed
     */
    public static function extractWhereValueFromQuery($query, $column)
    {
        return collect( $query->getQuery()->wheres )
            ->filter(function ($where) use ($column) {
                return $where['column'] === $column;
            })
            ->pluck('value')
            ->first();
    }

    /**
     * Resolve a variable to a Carbon instance.
     *
     * @param  mixed $date
     * @return Carbon\Carbon
     */
    public static function resolveDate($date)
    {
        if (is_string($date)) {
            $date = Carbon::parse($date);
        }

        if ($date instanceof Carbon === false) {
            return Carbon::now();
        }

        return $date;
    }
}


Expected result:
----------------
$ php artisan tinker
Psy Shell v0.8.3 (PHP 7.0.17 — cli) by Justin Hileman
>>> App\Shop::opened('2017-04-13')
=> App\Shop {#970
     owner_id: null,
     day_of_the_week: 4,
     start_date: "2017-04-13",
     end_date: "2017-04-13",
   }


Actual result:
--------------
$ php artisan tinker
Psy Shell v0.8.3 (PHP 7.0.17 — cli) by Justin Hileman
>>> App\Shop::opened('2017-04-13')
ErrorException with message 'Non-static method App\Shop::opened() should not be called statically'


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2017-04-13 01:53 UTC] requinix@php.net
-Status: Open +Status: Wont fix
 [2017-04-13 01:53 UTC] requinix@php.net
> I added to my class a non-static method intentionally expecting that if it is
> called statically, it will direct to the __callStatic() magic method.
That's not how methods work. There is only one with a given name in a class and it is an instance method or a static method. Calling the method incorrectly will result in an error.

Meanwhile __call and __callStatic intercept calls to methods that do not exist at all, or that don't exist from the perspective of the caller (ie, accounting for visibility). Static vs. instance is not a factor.

I just gave you a hint for how you could make this thing work - but only a hint because the whole scheme of making Shop::opened() result in $shop->opened() makes me nauseous.
 [2017-04-13 08:32 UTC] adrianrb93 at gmail dot com
-Status: Wont fix +Status: Closed
 [2017-04-13 08:32 UTC] adrianrb93 at gmail dot com
First, thank you for explaining the magic methods, I learned something new.

The example given is a dumbed down example for what I'm actually doing, I do understand the nausea you feel.

In Laravel you know the `scopeOpened($query)` can be called statically as opened() with the query builder passed into it. The intent of the method I was making was "I'm making a new record", and to make it a bit more expressive, I wanted it to work from the query builder and model.

I had solved the problem. This is what I had written to make it work:

```
    public static function handleCallContext($staticCallback, $objectCallback)
    {
        $instance = collect(
            debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit = 3)
        )->pluck('object')->last();

        if ($instance && get_class($instance) === __CLASS__) {
            return $objectCallback($instance);
        }

        return $staticCallback(new static);
    }
```
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Wed May 15 18:01:34 2024 UTC