Object Property, Method, And Static Access
Instance Access With ->
-> reaches a property or method on a specific object instance:
<?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
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
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::, andparent:: - 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
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
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.ENVis a class constant, so it is accessed with::. You may write$c::ENV(via an instance) orConfig::ENV(via the class name); both reach the same class constant. This is why it works despite$cbeing an instance — constants live on the class.echo Config->path;— error.->needs an object instance on the left, butConfigis a class name.pathis an instance property and requires an actual object:$c->path.echo $c::default();— works but is stylistically confusing.default()is a static method, properly calledConfig::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::$pathmeans "the static property namedpathonConfig," butpathis an instance property, not static. There is no static$path, hence the error. (Note the trap:Config::$pathis static-property syntax, not "the path property via the class.")
Corrected intentions:
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
staticproperty — is class-wide sharing actually intended, or should this be instance state? self::vsstatic::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::wherestatic::was intended silently breaks subclass overrides. Check that the choice is deliberate.- Dynamic member access from untrusted names.
$obj->$nameor$obj->$name()with$namefrom 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.