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
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
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

Pull Requests

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-2025 The PHP Group
All rights reserved.
Last updated: Sat Apr 26 02:01:28 2025 UTC