Table Of Contents
Problem
You need to reference objects without preventing garbage collection, or want to avoid memory leaks caused by circular references and observer patterns.
Solution
// Basic WeakReference usage
class User {
public function __construct(public string $name) {}
}
$user = new User('John');
$weakRef = WeakReference::create($user);
// Check if object still exists
if ($weakRef->get() !== null) {
echo $weakRef->get()->name; // 'John'
}
// After object is destroyed
unset($user);
// Force garbage collection for demonstration
gc_collect_cycles();
if ($weakRef->get() === null) {
echo "Object has been garbage collected\n";
}
// Observer pattern with WeakReference
class EventDispatcher {
private array $listeners = [];
public function addListener(string $event, object $listener, string $method): void {
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = [
'listener' => WeakReference::create($listener),
'method' => $method
];
}
public function dispatch(string $event, mixed $data = null): void {
if (!isset($this->listeners[$event])) {
return;
}
// Clean up dead references and call active listeners
$this->listeners[$event] = array_filter(
$this->listeners[$event],
function($listenerData) use ($data) {
$listener = $listenerData['listener']->get();
if ($listener === null) {
return false; // Remove dead reference
}
// Call the listener method
$method = $listenerData['method'];
$listener->$method($data);
return true; // Keep active reference
}
);
}
public function getListenerCount(string $event): int {
if (!isset($this->listeners[$event])) {
return 0;
}
// Count only active listeners
return count(array_filter(
$this->listeners[$event],
fn($data) => $data['listener']->get() !== null
));
}
}
class EmailNotifier {
public function onUserRegistered(array $userData): void {
echo "Sending email to: " . $userData['email'] . "\n";
}
}
class AnalyticsTracker {
public function onUserRegistered(array $userData): void {
echo "Tracking user registration: " . $userData['name'] . "\n";
}
}
$dispatcher = new EventDispatcher();
$emailNotifier = new EmailNotifier();
$analytics = new AnalyticsTracker();
$dispatcher->addListener('user.registered', $emailNotifier, 'onUserRegistered');
$dispatcher->addListener('user.registered', $analytics, 'onUserRegistered');
echo "Listeners before: " . $dispatcher->getListenerCount('user.registered') . "\n"; // 2
$dispatcher->dispatch('user.registered', ['name' => 'John', 'email' => 'john@example.com']);
// Remove one listener by destroying the object
unset($analytics);
gc_collect_cycles();
$dispatcher->dispatch('user.registered', ['name' => 'Jane', 'email' => 'jane@example.com']);
echo "Listeners after: " . $dispatcher->getListenerCount('user.registered') . "\n"; // 1
// WeakMap for object metadata
$userMetadata = new WeakMap();
class UserService {
private WeakMap $cache;
private WeakMap $accessLog;
public function __construct() {
$this->cache = new WeakMap();
$this->accessLog = new WeakMap();
}
public function getUserData(User $user): array {
// Check cache first
if (isset($this->cache[$user])) {
$this->logAccess($user, 'cache_hit');
return $this->cache[$user];
}
// Simulate database fetch
$data = [
'id' => rand(1000, 9999),
'name' => $user->name,
'email' => strtolower($user->name) . '@example.com',
'created_at' => date('Y-m-d H:i:s'),
'last_login' => date('Y-m-d H:i:s', strtotime('-' . rand(1, 30) . ' days'))
];
// Cache the data
$this->cache[$user] = $data;
$this->logAccess($user, 'database_fetch');
return $data;
}
private function logAccess(User $user, string $type): void {
if (!isset($this->accessLog[$user])) {
$this->accessLog[$user] = [];
}
$this->accessLog[$user][] = [
'type' => $type,
'timestamp' => microtime(true)
];
}
public function getAccessLog(User $user): array {
return $this->accessLog[$user] ?? [];
}
public function getCacheSize(): int {
return count($this->cache);
}
}
$userService = new UserService();
$user1 = new User('Alice');
$user2 = new User('Bob');
// Generate some data and access logs
$data1 = $userService->getUserData($user1);
$data1 = $userService->getUserData($user1); // Cache hit
$data2 = $userService->getUserData($user2);
echo "Cache size: " . $userService->getCacheSize() . "\n"; // 2
// When user1 is destroyed, cache entry is automatically removed
unset($user1);
gc_collect_cycles();
echo "Cache size after user1 destroyed: " . $userService->getCacheSize() . "\n"; // 1
// Parent-child relationship without circular references
class Department {
public function __construct(
public string $name,
private WeakMap $employees
) {
$this->employees = new WeakMap();
}
public function addEmployee(Employee $employee): void {
$this->employees[$employee] = [
'added_at' => time(),
'role' => $employee->role
];
// Set department without creating circular reference
$employee->setDepartment(WeakReference::create($this));
}
public function getEmployees(): array {
$employees = [];
foreach ($this->employees as $employee => $data) {
$employees[] = [
'employee' => $employee,
'data' => $data
];
}
return $employees;
}
public function getEmployeeCount(): int {
return count($this->employees);
}
}
class Employee {
private ?WeakReference $department = null;
public function __construct(
public string $name,
public string $role
) {}
public function setDepartment(WeakReference $department): void {
$this->department = $department;
}
public function getDepartment(): ?Department {
return $this->department?->get();
}
public function getDepartmentName(): string {
$dept = $this->getDepartment();
return $dept ? $dept->name : 'No Department';
}
}
$engineering = new Department('Engineering', new WeakMap());
$employee1 = new Employee('John Doe', 'Developer');
$employee2 = new Employee('Jane Smith', 'Designer');
$engineering->addEmployee($employee1);
$engineering->addEmployee($employee2);
echo "Department: " . $employee1->getDepartmentName() . "\n"; // Engineering
echo "Employee count: " . $engineering->getEmployeeCount() . "\n"; // 2
// Cache implementation with automatic cleanup
class ObjectCache {
private WeakMap $cache;
private WeakMap $metadata;
public function __construct() {
$this->cache = new WeakMap();
$this->metadata = new WeakMap();
}
public function set(object $key, mixed $value, int $ttl = 3600): void {
$this->cache[$key] = $value;
$this->metadata[$key] = [
'created_at' => time(),
'ttl' => $ttl,
'access_count' => 0
];
}
public function get(object $key): mixed {
if (!isset($this->cache[$key])) {
return null;
}
$metadata = $this->metadata[$key];
// Check TTL
if (time() > ($metadata['created_at'] + $metadata['ttl'])) {
unset($this->cache[$key]);
unset($this->metadata[$key]);
return null;
}
// Update access count
$this->metadata[$key]['access_count']++;
return $this->cache[$key];
}
public function has(object $key): bool {
return $this->get($key) !== null;
}
public function getStats(): array {
$totalEntries = count($this->cache);
$totalAccesses = 0;
foreach ($this->metadata as $data) {
$totalAccesses += $data['access_count'];
}
return [
'entries' => $totalEntries,
'total_accesses' => $totalAccesses,
'average_accesses' => $totalEntries > 0 ? $totalAccesses / $totalEntries : 0
];
}
}
// Temporary object registry
class TemporaryObjectRegistry {
private WeakMap $registry;
public function __construct() {
$this->registry = new WeakMap();
}
public function register(object $obj, array $metadata = []): string {
$id = uniqid('obj_', true);
$this->registry[$obj] = array_merge($metadata, [
'id' => $id,
'registered_at' => time(),
'class' => get_class($obj)
]);
return $id;
}
public function getMetadata(object $obj): ?array {
return $this->registry[$obj] ?? null;
}
public function isRegistered(object $obj): bool {
return isset($this->registry[$obj]);
}
public function getRegisteredObjects(): array {
$objects = [];
foreach ($this->registry as $obj => $metadata) {
$objects[] = [
'object' => $obj,
'metadata' => $metadata
];
}
return $objects;
}
}
$registry = new TemporaryObjectRegistry();
$tempUser = new User('Temporary User');
$userId = $registry->register($tempUser, ['type' => 'test', 'temporary' => true]);
echo "Registered object with ID: $userId\n";
echo "Is registered: " . ($registry->isRegistered($tempUser) ? 'Yes' : 'No') . "\n";
// Object becomes eligible for GC when $tempUser is unset
unset($tempUser);
gc_collect_cycles();
echo "Registry size after cleanup: " . count($registry->getRegisteredObjects()) . "\n";
// Memory usage comparison
function memoryUsageExample() {
$objects = [];
$weakRefs = [];
$normalRefs = [];
// Create objects
for ($i = 0; $i < 1000; $i++) {
$obj = new User("User $i");
$objects[] = $obj;
$weakRefs[] = WeakReference::create($obj);
$normalRefs[] = $obj; // This creates a second reference
}
$memoryBefore = memory_get_usage();
// Clear the original objects array
unset($objects);
gc_collect_cycles();
$memoryAfter = memory_get_usage();
echo "Memory difference: " . ($memoryBefore - $memoryAfter) . " bytes\n";
// Count active weak references
$activeWeakRefs = count(array_filter($weakRefs, fn($ref) => $ref->get() !== null));
echo "Active weak references: $activeWeakRefs\n"; // Should be 1000 (still referenced by $normalRefs)
// Clear normal references
unset($normalRefs);
gc_collect_cycles();
$activeWeakRefs = count(array_filter($weakRefs, fn($ref) => $ref->get() !== null));
echo "Active weak references after cleanup: $activeWeakRefs\n"; // Should be 0
}
memoryUsageExample();
Explanation
WeakReference and WeakMap create references that don't prevent garbage collection. Use them for observer patterns, caches, and avoiding circular references.
WeakReferences automatically become null when the referenced object is destroyed. WeakMaps automatically remove entries when the key object is garbage collected, preventing memory leaks.
Share this article
Add Comment
No comments yet. Be the first to comment!