Skip to content
beginner Phase 5 · PHP OOP

PHP Classes: Properties, Methods, and Constructors

Master PHP classes including declaration, properties, methods, constructors, visibility (public/private/protected), and static methods.

1h
0 problems
Topic Progress 0%

Class Declaration and Properties

Basic Class Declaration

<?php
namespace Vendor\Module\Model;

class Product
{
    // Properties with type declarations (PHP 7.4+)
    public int $id;
    public string $name;
    public float $price;
    public string $sku;
    public int $status;        // 1=enabled, 2=disabled
    public ?string $description = null;
    private ?\DateTimeImmutable $createdAt = null;
    protected array $metadata = [];

    // Constructor (PHP 8.0+ promoted properties)
    public function __construct(
        public string $name,
        public float $price,
        public string $sku,
    ) {
        $this->createdAt = new \DateTimeImmutable();
        $this->status = 1; // Default to enabled
    }

    // Methods
    public function getFormattedPrice(): string
    {
        return '\$' . number_format($this->price, 2);
    }

    public function isActive(): bool
    {
        return $this->status === 1;
    }

    // Getter pattern
    public function getDescription(): string
    {
        return $this->description ?? 'No description available';
    }

    // Setter pattern
    public function setPrice(float $price): self
    {
        if ($price < 0) {
            throw new \InvalidArgumentException('Price cannot be negative');
        }
        $this->price = $price;
        return $this;  // Enable method chaining
    }

    // Method chaining
    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function setSku(string $sku): self
    {
        $this->sku = $sku;
        return $this;
    }
}

// Usage with method chaining
$product = (new Product('Widget', 29.99, 'WDG-001'))
    ->setName('Widget Pro')
    ->setPrice(39.99);

echo $product->getFormattedPrice(); // '$39.99'

PHP 8 Constructor Promotion

<?php
// Before PHP 8 - verbose
class ProductOld
{
    private string $name;
    private float $price;
    private string $sku;

    public function __construct(string $name, float $price, string $sku)
    {
        $this->name = $name;
        $this->price = $price;
        $this->sku = $sku;
    }
}

// PHP 8+ - constructor promotion
class Product
{
    public function __construct(
        public readonly string $name,
        public readonly float $price,
        public readonly string $sku,
    ) {
        // Constructor body for additional logic
    }
}

// readonly properties can only be set once (in constructor)
$product = new Product('Widget', 29.99, 'WDG-001');
// $product->price = 49.99; // Error! readonly property

Property Types and Defaults

<?php
class ProductConfig
{
    // Typed properties with defaults
    public string $name = '';
    public float $price = 0.0;
    public int $stock = 0;
    public bool $active = true;
    public array $tags = [];
    public ?string $image = null;

    // Readonly properties (PHP 8.1+)
    public readonly int $id;
    public readonly \DateTimeImmutable $createdAt;

    public function __construct(int $id)
    {
        $this->id = $id;
        $this->createdAt = new \DateTimeImmutable();
    }
}

Key Takeaway

PHP classes use properties with type declarations and visibility modifiers. PHP 8 constructor promotion reduces boilerplate code. Use readonly for immutable properties.

Visibility Modifiers

Public, Private, Protected

<?php
namespace Vendor\Module\Model;

class Product
{
    // PUBLIC - accessible from anywhere
    public string $name;
    public float $price;

    // PRIVATE - accessible only within this class
    private string $internalCode;
    private array $priceHistory = [];

    // PROTECTED - accessible within this class and child classes
    protected int $status;
    protected array $metadata = [];

    public function __construct(string $name, float $price)
    {
        $this->name = $name;
        $this->price = $price;
        $this->internalCode = $this->generateCode($name);
        $this->status = 1;
    }

    // Private method - only called within this class
    private function generateCode(string $name): string
    {
        return strtoupper(substr($name, 0, 3)) . '-' . rand(100, 999);
    }

    // Private method for internal tracking
    private function recordPrice(float $oldPrice, float $newPrice): void
    {
        $this->priceHistory[] = [
            'old' => $oldPrice,
            'new' => $newPrice,
            'date' => date('Y-m-d H:i:s')
        ];
    }

    // Public method that uses private internals
    public function setPrice(float $newPrice): self
    {
        $oldPrice = $this->price;
        $this->price = $newPrice;
        $this->recordPrice($oldPrice, $newPrice);
        return $this;
    }

    // Protected method - accessible by children
    protected function validateStatus(int $status): bool
    {
        return in_array($status, [1, 2, 3]);
    }

    // Public interface
    public function setStatus(int $status): self
    {
        if (!$this->validateStatus($status)) {
            throw new \InvalidArgumentException('Invalid status');
        }
        $this->status = $status;
        return $this;
    }
}

Visibility Rules

<?php
// Access from outside the class
$product = new Product('Widget', 29.99);

$product->name = 'Gadget';       // OK - public
$product->price = 49.99;         // OK - public
// $product->internalCode = 'X'; // ERROR - private
// $product->status = 2;         // ERROR - protected

// Access from child class
class DiscountedProduct extends Product
{
    public function applyDiscount(float $percent): self
    {
        // $this->name = 'Discounted Widget';  // OK - public
        // $this->status = 1;                    // OK - protected
        // $this->internalCode = 'DISC-001';     // ERROR - private
        $this->price *= (1 - $percent / 100);
        return $this;
    }
}

// Access from same namespace (NO - PHP doesn't have package-private)
class AnotherProduct
{
    public function test(Product $product): void
    {
        // $product->internalCode; // ERROR - private
        // $product->status;        // ERROR - protected
        $product->name;             // OK - public
    }
}

Best Practices

<?php
class Product
{
    // PUBLIC: API surface - what consumers need
    public function __construct(
        public readonly string $name,
        public readonly float $price,
    ) {}

    // PRIVATE: Internal implementation details
    private ?string $cachedFormattedPrice = null;

    // PROTECTED: Extension points for child classes
    protected function calculateDiscountedPrice(float $percent): float
    {
        return $this->price * (1 - $percent / 100);
    }

    // Public method uses private cache
    public function getFormattedPrice(): string
    {
        if ($this->cachedFormattedPrice === null) {
            $this->cachedFormattedPrice = '\$' . number_format($this->price, 2);
        }
        return $this->cachedFormattedPrice;
    }
}
Modifier Same Class Child Class Outside
public Yes Yes Yes
protected Yes Yes No
private Yes No No

Key Takeaway

Use public for the API surface, protected for extension points, and private for implementation details. This encapsulation protects internal state and makes code maintainable.

Static Methods, Magic Methods, and Magento Patterns

Static Methods and Properties

<?php
namespace Vendor\Module\Model;

class ProductCounter
{
    // Static property - shared across all instances
    private static int $count = 0;

    public function __construct()
    {
        self::$count++;
    }

    // Static method - called without instantiation
    public static function getCount(): int
    {
        return self::$count;
    }

    public static function reset(): void
    {
        self::$count = 0;
    }
}

// Usage
$a = new ProductCounter();
$b = new ProductCounter();
echo ProductCounter::getCount(); // 2

Magic Methods

<?php
namespace Vendor\Module\Model;

class Product
{
    private array $data = [];

    // __get - called when accessing undefined property
    public function __get(string $name)
    {
        return $this->data[$name] ?? null;
    }

    // __set - called when setting undefined property
    public function __set(string $name, mixed $value): void
    {
        $this->data[$name] = $value;
    }

    // __isset - called with isset() or empty()
    public function __isset(string $name): bool
    {
        return isset($this->data[$name]);
    }

    // __unset - called with unset()
    public function __unset(string $name): void
    {
        unset($this->data[$name]);
    }

    // __toString - called when object is used as string
    public function __toString(): string
    {
        return $this->data['name'] ?? 'Unknown Product';
    }

    // __call - called when invoking undefined method
    public function __call(string $method, array $args)
    {
        // Dynamic method handling
        if (str_starts_with($method, 'get')) {
            $property = lcfirst(substr($method, 3));
            return $this->data[$property] ?? null;
        }
        throw new \BadMethodCallException("Method $method not found");
    }
}

// Usage
$product = new Product();
$product->name = 'Widget';  // Uses __set
echo $product->name;         // Uses __get
echo $product;               // Uses __toString
echo $product->getName();   // Uses __call

Magento OOP Patterns

<?php
namespace Vendor\Module\Model;

use Magento\Catalog\Model\Product as MagentoProduct;

class ProductRepository
{
    // Dependency injection via constructor
    public function __construct(
        private \Magento\Catalog\Model\ResourceModel\Product\Collection $collection,
        private \Magento\Framework\Event\ManagerInterface $eventManager,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    // Repository pattern
    public function getById(int $id): ?MagentoProduct
    {
        try {
            return $this->collection->load($id);
        } catch (\Exception $e) {
            $this->logger->error('Product load failed: ' . $e->getMessage());
            return null;
        }
    }

    // Factory pattern
    public function create(array $data): MagentoProduct
    {
        $product = $this->productFactory->create();
        $product->setData($data);
        return $product;
    }

    // Save with event dispatch
    public function save(MagentoProduct $product): void
    {
        $this->eventManager->dispatch('catalog_product_save_before', [
            'product' => $product
        ]);

        $product->save();

        $this->eventManager->dispatch('catalog_product_save_after', [
            'product' => $product
        ]);
    }
}

Object Comparison

<?php
$a = new Product('Widget', 29.99);
$b = new Product('Widget', 29.99);

// == compares all properties
echo $a == $b;   // true (same values)

// === compares identity (same instance)
echo $a === $b;  // false (different objects)

$b = $a;
echo $a === $b;  // true (same reference)

Key Takeaway

Static methods belong to the class, not instances. Magic methods enable dynamic behavior (getters/setters). Magento uses dependency injection, repository, and factory patterns extensively.

Quiz

1. What is the difference between public, private, and protected?

Question 1 options

2. What does PHP 8 constructor promotion do?

Question 2 options

3. What is a static method?

Question 3 options

4. What does the __get magic method do?

Question 4 options

5. What is the readonly keyword (PHP 8.1)?

Question 5 options

Flashcards

Question

What are the three visibility modifiers in PHP?

Answer

public (accessible anywhere), protected (accessible in class and children), private (accessible only in the class itself).

Question

What is constructor promotion (PHP 8)?

Answer

Shorthand for declaring and assigning properties in the constructor. Example: public readonly string $name creates property and assigns it.

Question

What is a static method?

Answer

A method that belongs to the class, not instances. Called without instantiation: ClassName::method(). Shared state via static properties.

Question

What does __toString() do?

Answer

Magic method called when an object is used as a string. Must return a string. Enables: echo $object; or 'text ' . $object.

Question

What is method chaining?

Answer

Returning $this from methods allows calling multiple methods in sequence. Example: $obj->setName('X')->setPrice(10)->save();

Question

What is the readonly keyword?

Answer

Makes a property settable only once (usually in constructor). Prevents modification after initialization. Useful for immutable data.

Question

What does == vs === mean for objects?

Answer

==: compares all properties (value equality). ===: compares object identity (same instance in memory).

Question

What magic method is called for undefined property access?

Answer

__get is called for reading, __set for writing, __isset for isset()/empty(), __unset for unset().

Revision Notes

Key Takeaways

  • 1. Public: accessible from anywhere. Protected: class + children. Private: class only.
  • 2. PHP 8 constructor promotion reduces boilerplate for property assignment
  • 3. Static methods/properties belong to the class, not instances
  • 4. Magic methods (__get, __set, __toString, __call) enable dynamic behavior
  • 5. readonly properties can only be set once (usually in constructor)
  • 6. Method chaining returns $this for fluent API design
  • 7. Magento uses DI, Repository, and Factory patterns extensively

Interview Tips

  • Explain the difference between public, private, and protected
  • Describe how constructor promotion works in PHP 8
  • Know when to use static vs instance methods
  • Explain magic methods and their use cases
  • Understand Magento's DI and repository patterns

Cheat Sheet

PHP Classes Cheat Sheet

Class Declaration:

class Product {
    public string $name;
    private float $price;
    protected int $status;
}

Constructor Promotion (PHP 8):

public function __construct(
    public readonly string $name,
    public readonly float $price,
) {}

Visibility:

  • public: anywhere
  • protected: class + children
  • private: class only

Static:

static $count = 0;
static function getCount(): int { return self::$count; }

Magic Methods:
__get, __set, __isset, __unset
__toString, __call, __construct, __destruct

Method Chaining:
return $this; // Enables $obj->a()->b()->c();