Lesson 4.2: Properties, Methods, and Visibility
Visibility in object-oriented programming is like having different levels of security clearance in an office building. Some information is public and available to everyone, some is private and only accessible to specific people, and some is protected and shared only within certain groups. Understanding visibility helps you build secure, well-designed classes that protect important data while providing clean interfaces for other code to use.
Think about your bank account. You can check your balance and make deposits (public operations), but you can't directly modify the bank's internal accounting systems or access other people's accounts (private operations). The bank carefully controls what you can and cannot do, ensuring security while providing the functionality you need.
In PHP classes, visibility modifiers control how properties and methods can be accessed from different parts of your code. This isn't just about security—it's about creating clear contracts between different parts of your application and preventing accidental misuse of class internals.
Professional PHP applications rely heavily on proper visibility design. When you work with frameworks like Laravel or libraries like Doctrine, you're interacting with carefully designed APIs that use visibility to provide clean, safe interfaces while protecting complex internal logic.
Understanding Visibility Modifiers
PHP provides three visibility levels: public, private, and protected. Each serves a specific purpose in controlling access to class members and creating well-structured object-oriented code.
Public members can be accessed from anywhere—inside the class, from other classes, or from procedural code. Private members can only be accessed from within the same class. Protected members can be accessed from within the class and its subclasses (which we'll explore in the inheritance lesson).
<?php
class BankAccount {
public $accountNumber; // Anyone can read this
private $balance; // Only this class can access
protected $accountType; // This class and subclasses
public function __construct($accountNumber, $initialBalance) {
$this->accountNumber = $accountNumber;
$this->balance = $initialBalance;
$this->accountType = "Standard";
}
// Public method - external code can call this
public function getBalance() {
return $this->balance;
}
// Public method - external code can call this
public function deposit($amount) {
if ($this->validateAmount($amount)) {
$this->balance += $amount;
return true;
}
return false;
}
// Private method - only this class can call this
private function validateAmount($amount) {
return is_numeric($amount) && $amount > 0;
}
}
$account = new BankAccount("12345", 1000);
// This works - public property
echo "Account: " . $account->accountNumber . "\n";
// This works - public method
echo "Balance: $" . $account->getBalance() . "\n";
// This would cause an error - private property
// echo $account->balance; // Fatal error!
// This would cause an error - private method
// $account->validateAmount(100); // Fatal error!
?>
Notice how external code can access the account number and balance through public interfaces, but cannot directly access the private balance property or validation method. This protects the account's internal state while providing controlled access.
Designing Public Interfaces
Public properties and methods form your class's interface—the contract that defines how other code can interact with your objects. Well-designed public interfaces are intuitive, consistent, and provide all necessary functionality without exposing implementation details.
<?php
class User {
private $id;
private $username;
private $email;
private $passwordHash;
private $loginAttempts;
private $isActive;
public function __construct($username, $email, $password) {
$this->id = uniqid();
$this->username = $username;
$this->email = $email;
$this->passwordHash = password_hash($password, PASSWORD_DEFAULT);
$this->loginAttempts = 0;
$this->isActive = true;
}
// Public getters provide controlled access to data
public function getId() {
return $this->id;
}
public function getUsername() {
return $this->username;
}
public function getEmail() {
return $this->email;
}
public function isActive() {
return $this->isActive;
}
// Public method for authentication
public function authenticate($password) {
if ($this->isAccountLocked()) {
return false;
}
if (password_verify($password, $this->passwordHash)) {
$this->resetLoginAttempts();
return true;
} else {
$this->incrementLoginAttempts();
return false;
}
}
// Public method for updating email
public function updateEmail($newEmail) {
if ($this->validateEmail($newEmail)) {
$this->email = $newEmail;
return true;
}
return false;
}
// Private helper methods
private function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
private function incrementLoginAttempts() {
$this->loginAttempts++;
}
private function resetLoginAttempts() {
$this->loginAttempts = 0;
}
private function isAccountLocked() {
return $this->loginAttempts >= 3;
}
}
$user = new User("alice_j", "[email protected]", "secure123");
// Clean public interface
echo "User ID: " . $user->getId() . "\n";
echo "Username: " . $user->getUsername() . "\n";
echo "Email: " . $user->getEmail() . "\n";
// Authentication through public method
if ($user->authenticate("wrongpassword")) {
echo "Login successful\n";
} else {
echo "Login failed\n";
}
// Update email through public method
if ($user->updateEmail("[email protected]")) {
echo "Email updated successfully\n";
}
?>
This design protects sensitive data like password hashes and login attempts while providing clean, safe methods for external code to interact with user objects.
Private Properties and Methods
Private members are the internal workings of your class—the data and methods that support public functionality but shouldn't be accessed directly from outside the class. Think of them as the engine of a car: essential for operation but not something drivers interact with directly.
<?php
class ShoppingCart {
private $items;
private $customerId;
private $sessionId;
private $taxRate;
private $discountPercentage;
public function __construct($customerId, $taxRate = 0.08) {
$this->items = [];
$this->customerId = $customerId;
$this->sessionId = session_id();
$this->taxRate = $taxRate;
$this->discountPercentage = 0;
}
public function addItem($productId, $name, $price, $quantity = 1) {
if ($this->validateProduct($productId, $price, $quantity)) {
$this->items[$productId] = [
'name' => $name,
'price' => $price,
'quantity' => $quantity
];
return true;
}
return false;
}
public function removeItem($productId) {
if (isset($this->items[$productId])) {
unset($this->items[$productId]);
return true;
}
return false;
}
public function getTotal() {
$subtotal = $this->calculateSubtotal();
$discount = $this->calculateDiscount($subtotal);
$tax = $this->calculateTax($subtotal - $discount);
return $subtotal - $discount + $tax;
}
public function getItemCount() {
return count($this->items);
}
public function applyDiscount($percentage) {
if ($this->validateDiscountPercentage($percentage)) {
$this->discountPercentage = $percentage;
return true;
}
return false;
}
// Private methods handle internal calculations
private function calculateSubtotal() {
$total = 0;
foreach ($this->items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
private function calculateDiscount($subtotal) {
return $subtotal * ($this->discountPercentage / 100);
}
private function calculateTax($amount) {
return $amount * $this->taxRate;
}
private function validateProduct($productId, $price, $quantity) {
return !empty($productId) &&
is_numeric($price) &&
$price > 0 &&
is_int($quantity) &&
$quantity > 0;
}
private function validateDiscountPercentage($percentage) {
return is_numeric($percentage) &&
$percentage >= 0 &&
$percentage <= 100;
}
}
$cart = new ShoppingCart("customer123");
// Public interface is clean and simple
$cart->addItem("PROD001", "Laptop", 999.99, 1);
$cart->addItem("PROD002", "Mouse", 29.99, 2);
$cart->applyDiscount(10);
echo "Items in cart: " . $cart->getItemCount() . "\n";
echo "Total: $" . number_format($cart->getTotal(), 2) . "\n";
// Private methods are protected from external access
// $cart->calculateSubtotal(); // This would cause an error
?>
Private methods like calculateSubtotal()
and validateProduct()
handle the complex internal logic while keeping the public interface simple and focused.
Protected Members and Inheritance
Protected members occupy a middle ground between public and private. They're accessible within the class and any classes that inherit from it, but not from external code. This becomes powerful when you start using inheritance to create specialized versions of base classes.
<?php
class Vehicle {
protected $engine;
protected $fuel;
protected $speed;
private $vin;
public function __construct($vin) {
$this->vin = $vin;
$this->speed = 0;
$this->fuel = 100;
}
public function getSpeed() {
return $this->speed;
}
public function getFuel() {
return $this->fuel;
}
// Protected method can be used by subclasses
protected function consumeFuel($amount) {
$this->fuel = max(0, $this->fuel - $amount);
}
// Protected method can be overridden by subclasses
protected function calculateFuelConsumption($distance) {
return $distance * 0.1; // Base consumption rate
}
}
class Car extends Vehicle {
private $doors;
public function __construct($vin, $doors = 4) {
parent::__construct($vin);
$this->doors = $doors;
$this->engine = "Gasoline";
}
public function drive($distance) {
$fuelNeeded = $this->calculateFuelConsumption($distance);
if ($this->fuel >= $fuelNeeded) {
$this->consumeFuel($fuelNeeded);
$this->speed = 60; // Cars drive at 60 mph
echo "Driving $distance miles at {$this->speed} mph\n";
return true;
} else {
echo "Not enough fuel to drive $distance miles\n";
return false;
}
}
// Override protected method with car-specific logic
protected function calculateFuelConsumption($distance) {
return $distance * 0.08; // Cars are more fuel efficient
}
}
class Truck extends Vehicle {
private $loadCapacity;
public function __construct($vin, $loadCapacity) {
parent::__construct($vin);
$this->loadCapacity = $loadCapacity;
$this->engine = "Diesel";
}
public function drive($distance) {
$fuelNeeded = $this->calculateFuelConsumption($distance);
if ($this->fuel >= $fuelNeeded) {
$this->consumeFuel($fuelNeeded);
$this->speed = 45; // Trucks drive slower
echo "Driving $distance miles at {$this->speed} mph\n";
return true;
} else {
echo "Not enough fuel to drive $distance miles\n";
return false;
}
}
// Override with truck-specific fuel consumption
protected function calculateFuelConsumption($distance) {
return $distance * 0.15; // Trucks use more fuel
}
}
$car = new Car("CAR123456", 4);
$truck = new Truck("TRUCK789", 2000);
$car->drive(50);
echo "Car fuel remaining: " . $car->getFuel() . "%\n\n";
$truck->drive(50);
echo "Truck fuel remaining: " . $truck->getFuel() . "%\n";
?>
The protected consumeFuel()
and calculateFuelConsumption()
methods are shared between the base Vehicle
class and its subclasses, but each subclass can customize the behavior while maintaining the same interface.
Getters and Setters: Controlled Access
Getters and setters provide controlled access to private properties. Instead of making properties public, you create methods that can validate input, format output, or perform additional logic when data is accessed or modified.
<?php
class Product {
private $id;
private $name;
private $price;
private $category;
private $stock;
private $isActive;
public function __construct($name, $price, $category) {
$this->id = uniqid();
$this->name = $name;
$this->setPrice($price); // Use setter for validation
$this->category = $category;
$this->stock = 0;
$this->isActive = true;
}
// Getters provide read access
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
public function getPrice() {
return $this->price;
}
public function getFormattedPrice() {
return "$" . number_format($this->price, 2);
}
public function getStock() {
return $this->stock;
}
public function isInStock() {
return $this->stock > 0;
}
// Setters provide controlled write access
public function setPrice($price) {
if (!is_numeric($price) || $price < 0) {
throw new InvalidArgumentException("Price must be a positive number");
}
$this->price = $price;
}
public function setStock($stock) {
if (!is_int($stock) || $stock < 0) {
throw new InvalidArgumentException("Stock must be a non-negative integer");
}
$this->stock = $stock;
}
public function addStock($quantity) {
if (!is_int($quantity) || $quantity <= 0) {
throw new InvalidArgumentException("Quantity must be a positive integer");
}
$this->stock += $quantity;
}
public function removeStock($quantity) {
if (!is_int($quantity) || $quantity <= 0) {
throw new InvalidArgumentException("Quantity must be a positive integer");
}
if ($quantity > $this->stock) {
throw new InvalidArgumentException("Cannot remove more stock than available");
}
$this->stock -= $quantity;
}
public function activate() {
$this->isActive = true;
}
public function deactivate() {
$this->isActive = false;
}
public function isActive() {
return $this->isActive;
}
}
$product = new Product("Gaming Laptop", 1299.99, "Electronics");
$product->setStock(10);
echo "Product: " . $product->getName() . "\n";
echo "Price: " . $product->getFormattedPrice() . "\n";
echo "In stock: " . ($product->isInStock() ? "Yes" : "No") . "\n";
// Controlled stock management
$product->removeStock(3);
echo "Stock after sale: " . $product->getStock() . "\n";
// Validation prevents invalid data
try {
$product->setPrice(-50); // This will throw an exception
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
Getters and setters provide several advantages: they can validate input, format output, log access for debugging, and allow you to change internal implementation without breaking external code.
Static Properties and Methods
Static members belong to the class itself rather than to individual objects. They're useful for data or functionality that's shared across all instances of a class or that doesn't require an object to function.
<?php
class Database {
private static $connection = null;
private static $queryCount = 0;
private static $config = [
'host' => 'localhost',
'port' => 3306,
'database' => 'myapp'
];
// Static method for getting database connection
public static function getConnection() {
if (self::$connection === null) {
self::$connection = "Database connection established";
echo "New database connection created\n";
}
return self::$connection;
}
// Static method for configuration
public static function setConfig($key, $value) {
if (array_key_exists($key, self::$config)) {
self::$config[$key] = $value;
}
}
public static function getConfig($key = null) {
if ($key === null) {
return self::$config;
}
return self::$config[$key] ?? null;
}
// Static method for tracking queries
public static function executeQuery($sql) {
self::$queryCount++;
echo "Executing query #" . self::$queryCount . ": $sql\n";
return "Query result";
}
public static function getQueryCount() {
return self::$queryCount;
}
}
// Static methods called on the class, not objects
$connection = Database::getConnection();
Database::executeQuery("SELECT * FROM users");
Database::executeQuery("SELECT * FROM products");
echo "Total queries executed: " . Database::getQueryCount() . "\n";
// Configuration doesn't require an object
Database::setConfig('host', 'production-server.com');
echo "Database host: " . Database::getConfig('host') . "\n";
?>
Static members are perfect for utility functions, configuration management, design patterns like Singleton, and tracking shared state across all instances of a class.
Constants in Classes
Class constants define values that never change and are associated with the class. They're useful for configuration values, status codes, or any fixed data related to the class.
<?php
class OrderStatus {
const PENDING = 'pending';
const CONFIRMED = 'confirmed';
const SHIPPED = 'shipped';
const DELIVERED = 'delivered';
const CANCELLED = 'cancelled';
const VALID_STATUSES = [
self::PENDING,
self::CONFIRMED,
self::SHIPPED,
self::DELIVERED,
self::CANCELLED
];
}
class Order {
private $id;
private $customerId;
private $status;
private $items;
private $createdAt;
public function __construct($customerId) {
$this->id = uniqid();
$this->customerId = $customerId;
$this->status = OrderStatus::PENDING;
$this->items = [];
$this->createdAt = time();
}
public function getStatus() {
return $this->status;
}
public function updateStatus($newStatus) {
if (in_array($newStatus, OrderStatus::VALID_STATUSES)) {
$this->status = $newStatus;
echo "Order status updated to: $newStatus\n";
return true;
} else {
echo "Invalid status: $newStatus\n";
return false;
}
}
public function canBeCancelled() {
return in_array($this->status, [
OrderStatus::PENDING,
OrderStatus::CONFIRMED
]);
}
public function isCompleted() {
return $this->status === OrderStatus::DELIVERED;
}
}
$order = new Order("customer123");
echo "Initial status: " . $order->getStatus() . "\n";
$order->updateStatus(OrderStatus::CONFIRMED);
$order->updateStatus(OrderStatus::SHIPPED);
echo "Can be cancelled: " . ($order->canBeCancelled() ? "Yes" : "No") . "\n";
echo "Is completed: " . ($order->isCompleted() ? "Yes" : "No") . "\n";
?>
Class constants provide type safety and make code more maintainable by centralizing important values and preventing typos in status strings.
Key Takeaways
Visibility modifiers are essential for creating well-designed, secure classes. Use public for interfaces that external code needs, private for internal implementation details, and protected for functionality shared with subclasses.
Getters and setters provide controlled access to object data, allowing validation, formatting, and business logic while protecting internal state. They're preferable to public properties in most professional applications.
Static members belong to the class rather than instances and are perfect for utility functions, configuration, and shared state. Constants define unchanging values associated with the class.
Good visibility design makes your classes easier to use, more secure, and more maintainable. Think carefully about what needs to be public and keep everything else private or protected.
The principles you learn here apply to all modern PHP frameworks and libraries. Understanding visibility helps you use these tools effectively and write code that integrates well with professional PHP ecosystems.
In the next lesson, we'll explore inheritance and polymorphism, which build on these visibility concepts to create powerful, flexible class hierarchies.