PHP Mechanics And Reflection

Object Property, Method, And Static Access

Instance Access With ->

-> reaches a property or method on a specific object instance:

PHP example
<?php

declare(strict_types=1);

class Counter {
    public int $count = 0;
    public function increment(): void { $this->count++; }
}

$c = new Counter();
$c->increment();            // call a method on the instance
$c->increment();
echo $c->count . PHP_EOL;   // read a property on the instance

// Inside the class, $this-> reaches the current instance's own members.

// Prints:
// 2

Note $this->count inside the class — $this is the current instance, and its members are reached with -> just like from outside. A method and a property can even share a name; $c->count reads the property and $c->count() calls the method, because the parentheses disambiguate.

Class-Level Access With ::

:: (scope resolution) reaches things that belong to the class rather than any instance: constants, static properties, and static methods.

PHP example
<?php

declare(strict_types=1);

class Money {
    public const CURRENCY = 'USD';       // class constant
    public static int $count = 0;        // static property (shared by all)
    public static function make(): self {  // static method
        self::$count++;
        return new self();
    }
}

echo Money::CURRENCY . PHP_EOL;   // constant
Money::make();
Money::make();
echo Money::$count . PHP_EOL;     // static property, shared across all uses

// self:: refers to the class from inside it; a subclass may use static:: for late binding.

// Prints:
// USD
// 2

The keywords that pair with ::: self:: refers to the class it is written in, static:: enables late static binding (resolving to the actual called class in a subclass), and parent:: reaches the parent class's version. A static member exists once for the whole class, not per instance — which is exactly why Money::$count accumulates across separate make() calls.

Dynamic Property And Method Access

Just as array keys can be computed, property and method names can be held in variables — this is variable variables applied to objects, and it powers serializers, ORMs, and dependency injection:

PHP example
<?php

declare(strict_types=1);

class User {
    public string $name = 'Ada';
    public string $role = 'admin';
    public function greet(): string { return 'hi'; }
}

$u = new User();

$prop = 'name';
echo $u->$prop . PHP_EOL;       // dynamic property -> $u->name -> 'Ada'

$method = 'greet';
echo $u->$method() . PHP_EOL;   // dynamic method call -> $u->greet()

// Prints:
// Ada
// hi

$u->$prop reads the property whose name is the value of $prop. This is how foreach ($fields as $f) { $dto->$f = $input[$f]; } hydrates an object generically. The safety concern is identical to dynamic array keys: when the name comes from untrusted input, an attacker can reach properties or invoke methods you did not intend — validate names against an allow-list, and prefer the Reflection API for serious introspection, since it respects visibility and is designed for the job.

What To Check

Before moving on, make sure you can:

  • use -> for instance members and :: for class-level members
  • explain $this, self::, static::, and parent::
  • explain that a static member is shared once per class, not per instance
  • access properties and methods dynamically with a name held in a variable
  • validate dynamic member names from untrusted input against an allow-list

What You Should Be Able To Do

After this lesson, you should be able to choose -> versus :: correctly, work with static members and the self/static/parent keywords, and use dynamic property and method access for data-driven code while guarding against untrusted member names.

Practice

Practice: Hydrate An Object Dynamically
Show solution
PHP example
<?php

declare(strict_types=1);

class Profile {
    public string $name = '';
    public string $email = '';
    public bool $isAdmin = false;
}

function hydrate(object $target, array $data, array $allowed): void
{
    foreach ($data as $key => $value) {
        if (in_array($key, $allowed, true)) {
            $target->$key = $value;   // dynamic property write
        }
    }
}

$p = new Profile();

// Untrusted input tries to sneak isAdmin in:
$input = ['name' => 'Ada', 'email' => 'ada@example.com', 'isAdmin' => true];

hydrate($p, $input, ['name', 'email']);   // isAdmin NOT allowed

echo $p->name . PHP_EOL;
echo $p->email . PHP_EOL;
var_dump($p->isAdmin);   // still false — the disallowed key was ignored

// Prints:
// Ada
// ada@example.com
// bool(false)

$target->$key is dynamic property access: the property whose name is the value of $key gets set. The $allowed allow-list is the security control — it stops the classic mass-assignment vulnerability where untrusted input (isAdmin => true) sets a property the user should never control. Without the allow-list, this generic hydrator would happily grant admin. The lesson: dynamic member access is powerful precisely because it is generic, and that same genericity is why untrusted names must be constrained.

Practice: Diagnose An Access Operator Mix-Up
PHP example
class Config {
    public const ENV = 'prod';
    public string $path = '/etc/app';
    public static function default(): self { return new self(); }
}

$c = new Config();
echo $c::ENV;          // works, but they're unsure why
echo Config->path;     // error
echo $c::default();    // works, but confusingly
echo Config::$path;    // error

Task

Explain which lines are right, which are wrong, and the rule.

Show solution

Line by line:

  • echo $c::ENV;works. ENV is a class constant, so it is accessed with ::. You may write $c::ENV (via an instance) or Config::ENV (via the class name); both reach the same class constant. This is why it works despite $c being an instance — constants live on the class.
  • echo Config->path;error. -> needs an object instance on the left, but Config is a class name. path is an instance property and requires an actual object: $c->path.
  • echo $c::default();works but is stylistically confusing. default() is a static method, properly called Config::default(). PHP also permits calling a static method through an instance with ::, so $c::default() resolves to the same thing — legal but misleading, since it looks instance-bound while being static.
  • echo Config::$path;error. Config::$path means "the static property named path on Config," but path is an instance property, not static. There is no static $path, hence the error. (Note the trap: Config::$path is static-property syntax, not "the path property via the class.")

Corrected intentions:

PHP example
echo Config::ENV;        // constant via class
echo $c->path;           // instance property via object
echo Config::default();  // static method via class

The takeaway pairing: constants and statics ↔ :: (with a class name, or permissibly an instance); instance properties and instance methods ↔ -> (with an object). Mixing the operand kind and the member kind is the error, and Config::$path versus $c->path is the classic confusion.

Practice: Define Object Access Review Checks

List what to check in review regarding instance vs. static access, static-member sharing, and dynamic member access.

Show solution
  • Unintended shared state via statics. A static property is shared across the whole class, so mutable static state is effectively global and a common source of test pollution and concurrency surprises. Question every mutable static property — is class-wide sharing actually intended, or should this be instance state?
  • self:: vs static:: in inheritance. self:: binds to the defining class; static:: uses late static binding to resolve to the called class. In code meant to be extended, self:: where static:: was intended silently breaks subclass overrides. Check that the choice is deliberate.
  • Dynamic member access from untrusted names. $obj->$name or $obj->$name() with $name from input is a mass-assignment / arbitrary-method-call risk. Require an allow-list of permitted names, and prefer the Reflection API for genuine introspection since it respects visibility.
  • Visibility bypass expectations. Dynamic access still obeys visibility (private/protected), so code assuming it can reach a private property dynamically will fail — verify the intended members are actually accessible from that scope.

Static analysis catches most operator/member mismatches and undefined-member access; the review focus is the judgment calls: shared static state, self-vs-static binding, and untrusted dynamic names.