Lập trình hướng đối tượng trong PHP(Phần 2)

Xin chào tất cả mọi người. Hôm nay mình sẽ quay lại phần 2 trong bài viết về lập trình hướng đối tượng trong PHP. Trong phần 1 mình sẽ đề cập về ba vấn đề Các đặc điểm cơ bản của lập trình hướng đối tượng. Chúng được thể hiện như thế nào trong PHP, Sự khác biệt giữa Abstract Class và InterfaceThế nào là một hàm static, phân biệt cách dùng từ khoá static::method() với self::method().

Mục lục

Phần 1

  • Các đặc điểm cơ bản của lập trình hướng đối tượng. Chúng được thể hiện như thế nào trong PHP
  • Sự khác biệt giữa Abstract Class và Interface.
  • Thế nào là một hàm static. Phân biệt cách dùng từ khoá static::method() với self::method()

Phần 2

  • Thế nào là Trait
  • Thế nào là Namespaces
  • Thế nào là magic functions
  • Tìm hiểu về các quy tắc trong PSR-2

Phần 3

  • Các phương pháp thiết kế hướng đối tượng (SOLID)

Trong phần này, mình sẽ chia sẻ về 4 vấn tiếp theo trong loạt bài viết, đó là: Thế nào là Trait, Thế nào là Namespaces, Thế nào là magic functions, Tìm hiểu về các quy tắc trong PSR2.

Nội dung

4. Thế nào là Trait

Trait được giới thiệu trong PHP phiên bản 5.4.0 và được định nghĩa là một cơ chế cho phép lập trình viên tận dụng khả năng tái sử dụng lại code (code reusability) khi lập trình với ngôn ngữ chỉ cho phép thừa kế từ một class duy nhất (hay còn gọi là single inheritance) như PHP. Vậy Trait là gì? Trait là một module giúp cho chúng ta có thể sử dụng lại các phương thức được khai báo trong trait vào các class khác nhau hoặc trong các trait khác một cách đơn giản hơn là kế thừa như trước. Một trait tương tự như là 1 class nhưng chỉ nhằm mục đích nhóm chức năng lại. Và trait không thể khởi tạo giống class và trait sinh ra để bổ sung cho kế thừa truyền thống. Thay vì phải kế thừa 1 class hay interface để sử dụng lại 1 nhóm chức năng, thì với trait bạn không cần phải kế thừa vẫn có thể sử dụng được. Ví dụ:

<?php
class Database
{
	public function listUsers()
	{
		return "List User";
	}
}
/**
* 
*/
class Report extends Database
{
	
	public function reportUsers()
	{
		$this->listUsers();
	}
}
/**
* 
*/
class Users extends Database
{
	public function index()
	{
		$this->listUsers();
	}	
}

Đây là trường hợp không sử dụng trait và cũng không biết dùng trait ở đâu 😄. Mà ta thấy nếu có nếu có quá nhiều class thì việc extends lại sẽ làm ảnh hưởng rất nhiều đến công việc, tài nguyên, hiệu năng. Hướng giải quyết như sau:

<?php
trait Database
{
	public function listUsers()
	{
		return "List User";
	}
}
/**
* 
*/
class Users
{
	use Database;

	public function reportUsers()
	{
		$this->listUsers();
	}
}
/**
* 
*/
class Report
{
	use Database;

	public function reportUsers()
	{
		$this->listUsers();
	}
}

Các bạn thấy không, mình không cần extends nhưng vẫn có thể sử dụng lại các phương thức của method listUsers() như trên. Để sử dụng trait trong PHP thì các bạn chỉ cần dùng từ khóa use để gọi trait bạn muốn sử dụng trong code của bạn. Sau đó bạn có thể sử dụng các phương thức trong trait mà bạn đã use. Ví dụ ở trên mình use Database sau đó sẽ sử dụng lại được phương thức listUser() trong trait này À, đến đây, ta lại thấy, nếu muốn sử dụng nhiều phương thức trong nhiều trait thì làm như thế nào? Tiếp tục nhé.

<?php
trait Database
{
  public function sayHello()
  {
  	echo "hello";
  }
}
/**
* 
*/
trait World
{
  public function sayWorld()
  {
  	echo "World";
  }
}
/**
* 
*/
class MyHelloWorld
{
  use Database, World;

  public function sayHelloWorld() {
  	echo "!";
  }
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayHelloWorld();
//Kết quả : helloWorld!

Các bạn hãy nhìn hình nhé. Ở đây mình có 2 trait là Hello và World chứa lần lượt phương thức là sayHello()sayWorld(). Và việc sử dụng chúng rất đơn giản như là sử dụng một trait vậy. Bạn có thể use và sử dụng các phương thức bên trong nó như bình thường. Có vẻ mọi thứ đã ổn. Nhưng nếu một ngày không đẹp trời, các phương thức trong các trait giống nhau, chúng ta sẽ phải làm sao? Ở đây ta sẽ sử dụng insteadof để xét độ ưu tiên cho phương thức bạn muốn sử dụng. Ví dụ :

<?php
//trait SetGetName
trait SetGetName
{
   public function setName($name)
   {
   	$this->name = $name;
   }
   public function getName()
   {
   	return $this->name;
   }
   public function getAll()
   {
   	return $this->getName();
   }
   
}
//trait SetGetAge
trait SetGetAge
{
   public function setAge($age)
   {
   	$this->age = $age;
   }
   public function getAge()
   {
   	return $this->age;
   }
   public function getAll()
   {
   	return $this->getAge();
   }
}
class ConNguoi
{
   public $name;
   private $age;
   //gọi trait
   use SetGetName,SetGetAge
   {
   	//ưu tiên sử dụng phương thức getall của trait SetGetAge
   	SetGetAge::getAll insteadof SetGetName;
   }
}

Tiếp theo, chúng ta sẽ tìm hiểu về 1 phần rất quan trọng trong trait đó là trait lồng. Bạn đã bao giờ nghĩ nếu có 100 trait, ta sẽ use tận 100 lần. Điều này là không nên. Nhìn rất củ chuối =))) Ví dụ:

<?php
trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait World {
    public function sayWorld() {
        echo 'World!';
    }
}

trait HelloWorld {
    use Hello, World;
}

class MyHelloWorld {
    use HelloWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
// kết quả : Hello World!

Việc sử dụng trait lồng không quá phức tạp và là cách giúp cho code dễ nhìn, hiệu quả hơn.

5. Thế nào là namespace

  • Khái niệm: Theo php.net thì nó là cách để đóng gói những file php. Được thiết kế để giải quyết 2 vấn đề là thể hiện được tác quyền của thư viện đang sử dụng và sự đụng độ không gian tên trên những ứng dụng khi tạo ra những thành phần code tái sử dụng như các classes hay functions.

  • Định nghĩa: Được khai báo với từ khóa namespace. Mỗi file chứa 1 namespace phải được khai báo trên cùng trước các code khác trong file. Cách thực hành tốt nhất là mỗi file chứa 1 class với tên class trùng với tên file đồng thời có 1 namespace mô tả được đường dẫn tới file đó

  • Khai báo : Giống như đường dẫn tới thư mục. Cách thực hành tốt nhất là nên đặt namespaces theo cấu trúc thư mục chứa nó, điều này là tuyệt vời khi kết hợp với autoloading trong PHP. VD: Tạo 1 class HomeController và đặt tên như sau:

    namespace App\Controllers; 
    
    class HomeController {
      //code
    }
    

    Sử dụng name space:

    use App\Controllers\HomeController;
    

6. Thế nào là magic functions

Trong lập trình hướng đối tượng để làm cho việc sử dụng các đối tượng dễ dàng hơn, PHP cũng cung cấp một số Magic Method, hoặc các phương thức đặc biệt được gọi khi các hành động nhất định xảy ra trong các đối tượng. Điều này cho phép các lập trình viên thực hiện một số nhiệm vụ hữu ích tương đối dễ dàng. Trong bài này mình xin phép nói về 2 magic function quan trọng và hay dùng nhất trong lập trình hướng đối tượng trong PHP. Đó là ConstructorsDestructors. Trước hết mình xin đi vào Constructors, vậy Constructors là gì? Xét ví dụ sau:

<?php
/**
* 
*/
class ClassName
{
    public $example = "Đây là thuộc tính của class";
	
    public function __construct()
	{
		echo "Đối tượng vừa được khởi tạo! <br>";
	}
	
	public function setProperty($value)
	{
		$this->example = $value;
	}
	
	public function getProperty()
	{
		return $this->example . '<br/>';
	}
}

$obj = new ClassName();
echo $obj->getProperty();
// kết quả : 
//Đối tượng vừa được khởi tạo! 
//Đây là thuộc tính của class

Các bạn thấy gì nào? Sau khi khởi tạo đối tượng NameClass, lập tức hàm __construct() sẽ tự động chạy. Điều này giống như một cách thông báo đến người lập trình. Destructors: Bản chất ngược lại với Constructors. Ví dụ:

<?php
/**
* 
*/
class ClassName
{
    public $example = "Đây là thuộc tính của class";
	
    public function __construct()
	{
		echo "Đối tượng vừa được khởi tạo! <br>";
	}
	
	public function setProperty($value)
	{
		$this->example = $value;
	}
	
	public function getProperty()
	{
		return $this->example . '<br/>';
	}
	
	public function __destruct()
	{
		echo "Đối tượng vừa bị hủy! <br>";
	}
}

$obj = new ClassName();
echo $obj->getProperty();
echo "Kết thúc! ";

//kết quả:
//Đối tượng vừa được khởi tạo! 
//Đây là thuộc tính của class
//Kết thúc! Đối tượng vừa bị hủy! 

Để kích hoạt destructor một cách rõ ràng, bạn có thể hủy đối tượng Object ngay lập tức bằng cách sử dụng hàm unset() :

<?php
/**
* 
*/
class ClassName
{
    public $example = "Đây là thuộc tính của class";
	
    public function __construct()
	{
		echo "Đối tượng vừa được khởi tạo! <br>";
	}
	
	public function setProperty($value)
	{
		$this->example = $value;
	}
	
	public function getProperty()
	{
		return $this->example . '<br/>';
	}
	
	public function __destruct()
	{
		echo "Đối tượng vừa bị hủy! <br>";
	}
}

$obj = new ClassName();
echo $obj->getProperty();
unset($obj);
echo "Kết thúc! ";
//kết quả:
//Đối tượng vừa được khởi tạo! 
//Đây là thuộc tính của class
//Đối tượng vừa bị hủy! 
//Kết thúc!

Tiếp theo, chúng ta sẽ đi đến các hàm quan trọng khác... __get : sẽ tự động được gọi khi chúng ta lấy ra giá trị của các thuộc tính trong đối mà chúng ta không được phép truy cập nó từ bên ngoài hoặc không tồn tại. VD:

<?php 

class ConNguoi
{
 private $title = "Việt Nam vô địch" ;
   
 public function __get($key)
 {
     //kiểm tra xem trong class có tồn tại thuộc tính không
     if(property_exists($this, $key)){
       //tiến hành lấy giá trị
         return $this->$key;
     } else {
        die('Không tồn tại thuộc tính');
     }
 }
 public function getTitle()
 {
     echo $this->title;
 }
}
$vietnam = new ConNguoi();
echo $vietnam->title;
//Việt Nam vô địch
$vietnam->age;
//Không tồn tại thuộc tính

__set : sẽ tự động được gọi khi chúng ta thiết lập giá trị cho một thuộc tính không được phép truy cập từ bên ngoài, hoặc không tồn tại.

<?php 
class VietNam
{
 private $title;
   
 public function __set($key, $value)
 {
     //kiểm tra xem trong class có tồn tại thuộc tính không
     if(property_exists($this, $key)) {
       //tiến hành gán giá trị
       $this->$key = $value;
     } else {
        die('Không tồn tại thuộc tính');
     }
 }
 public function getTitle()
 {
     echo $this->title;
 }
}
$vietnam = new VietNam();
$vietnam->title = "Việt Nam vô địch";
$vietnam->getTitle();
//Việt Nam vô địch
$vietnam->age = 20;
//Không tồn tại thuộc tính

Ngoài ra còn một số magic funtions dưới đây mình xin phép được liệt kê dưới đây: __isset() : hàm isset, sẽ được gọi khi chúng ta thực hiện kiểm tra một thuộc tính không được phép truy cập của một đối tượng, hay kiểm tra một thuộc tính không tồn tại trong đối tượng đó. __unset(): sẽ được gọi khi chúng ta thực hiện hủy (unset) một thuộc tính không được phép truy cập của một đối tượng, hay hủy một thuộc tính không tồn tại trong đối tượng đó. Cụ thể là hàm unset(). __sleep() : Phương thức __sleep() sẽ được gọi khi chúng ta thực hiện serialize()đối tượng. Thông thường khi chúng ta serialize()một đối tượng thì nó sẽ trả về tất cả các thuộc tính trong đối tượng đó. Nhưng nếu sử dụng __sleep() thì chúng ta có thể quy định được các thuộc tính có thể trả về. __wakeup(): sẽ được gọi khi chúng ta unserialize() đối tượng. Chúng thường được sử dụng để thực thi một hoặc nhiều hành động nào đó khi đối tượng được unserialize(). __toString(): sẽ được gọi khi chúng ta dùng đối tượng như một string. __invoke(): sẽ được gọi khi chúng ta sử đối tượng như một hàm. __set_state(): Đây là phương thức ít được sử dụng nhất trong PHP và hầu như là không bao giờ. Phương thức này sẽ được gọi khi chúng ta var_export() đối tượng. __clone(): được gọi khi chúng ta clone object. __debugInfo(): được gọi khi chúng ta var_dump() đối tượng. Thông thường, khi chúng ta var_dump() một đối tượng thì nó sẽ trả về tất cả các thuộc tính và giá trị của nó trong đối tượng đó, nhưng khi chúng ta sử dụng phương thức __debugInfo()thì chúng ta có thể tùy chỉnh thông số trả về. __call(): hàm call, được gọi khi ta gọi đến một phương thức không tồn tại trong đối tượng __callStatic(): hàm call static, được gọi khi ta gọi đến một phương thức tĩnh không tồn tại trong đối tượng

Ưu và nhược điểm của magic functions :

  • Ưu điểm:
    • Giúp cho chúng ta tùy biến được các hành vi.
    • Nó giúp cho chúng ta có thể khởi tạo một đối tượng theo cách mình muốn.
  • Nhược điểm:
    • Tốc độ chậm hơn các function bình thường.

7. Các quy tắc trong PSR-2

Viết code chuẩn (coding convention) là chúng ta tuân thủ một quy định trong viết code của một tập thể hay một công ty dựa theo quy chuẩn trong lập trình. Tùy thuộc vào ngôn ngữ sẽ có chuẩn viết code khác nhau. Trong bài viết này mình sẽ nói về chuẩn code PSR-2 trong PHP.

PSR có nghĩa là PHP Standards Recommendations. Có rất nhiều PSR từ PSR-0 đến PSR-7. Trong đó PSR-1 và PSR-2 chúng ta sẽ tiếp xúc rất nhiều. PSR-1 sẽ giúp chúng ta biết thề nào và làm thế nào để đặt tên biến, tên hàm sau cho dể hiểu, dể đọc mang tính thống nhất toàn bộ.

Khác với PSR-1, PSR-2 sẽ mang tính trình bày là chính. Nó có nhiệm vụ rất quan trong trong việc trình bài các dòng code của bạn. từ các dòng tab hay xuống hàng giữa các dòng, các hàm một cách tỉ mỉ.

Vậy tại sao lại phải viết code chuẩn

  • Như mình đã đề cập ở trên việc viết code chuẩn rất quan trọng. đối với những ai thường không tuân theo quy định viết code chuẩn (viết tùy ý) đó chỉ có thể làm việc cá nhân.
  • Các bạn nên nhớ rằng khi đi làm chúng ta điều làm teamwork không thể mõi người một cách viết. Mõi người một cách viết là đúng khi nói về tư duy logic trong lập trình có thể cùng một kết quả nhưng lại có nhiều cách viết khác nhau.
  • Sự thống nhất về cách trình bày. Bạn viết như thế nào khi người khác xem lại hoặc sữa code của bạn vẫn hiểu được cái function của bạn đang viết cái gì phải không nào.

Các quy tắc:

  • Code PHẢI tuân thủ PSR-1
  • Code PHẢI sử dụng 4 ký tự space để lùi khối (không dùng tab)
  • Mỗi dòng code PHẢI dưới 120 ký tự, NÊN dưới 80 ký tự.
  • PHẢI có 1 dòng trắng sau namespace, và PHẢI có một dòng trắng sau mỗi khối code.
  • Ký tự mở lớp { PHẢI ở dòng tiếp theo, và đóng lớp } PHẢI ở dòng tiếp theo của thân class.
  • Ký tự { cho hàm PHẢI ở dòng tiếp theo, và ký tự } kết thúc hàm PHẢI ở dòng tiếp theo của thân hàm.
  • Các visibility (public, private, protected) PHẢI được khai báo cho tất cả các hàm và các thuộc tính của lớp;
  • Các từ khóa điều khiển khối(if, elseif, else) PHẢI có một khoảng trống sau chúng; hàm và lớp thì KHÔNG ĐƯỢC làm như vậy.
  • Mở khối { cho cấu trúc điều khiển PHẢI trên cùng một dòng; và đóng khối này với } ở dòng tiếp theo của thân khối.
  • Hằng số true, false, null PHẢI viết với chữ thường.
  • Từ khóa extends và implements PHẢI cùng dòng với class
  • Implements nhiều lớp, thì mỗi lớp trên một dòng
  • Keyword var KHÔNG ĐƯỢC dùng sử dụng khai báo property.
  • Tên property KHÔNG NÊN có tiền tố _ nhằm thể hiện thuộc tính protect hay private.
  • Tham số cho hàm, phương thức: KHÔNG được thêm space vào trước dấu , và PHẢI có một space sau ,. Các tham số CÓ THỂ trên nhiều dòng, nếu làm như vậy thì PHẢI mỗi dòng 1 tham số.
  • Abstract, final PHẢI đứng trước visibility, còn static phải đi sau.

Một số chi tiết về chuẩn PSR-1:

  • Các file phải sử dụng thẻ <?php hoặc <?.
  • File PHP chỉ sử dụng encode: UTF-8 without BOM với code PHP.
  • Các file nên dùng để khai báo các thành phần PHP (các lớp, hàm, hằng ...) hoặc dùng với mục đích làm hiệu ứng phụ (như include, thiết lập ini cho PHP ...), nhưng không nên dùng trong cả 2 trường hợp.
  • Các namespace và các class phải theo chuẩn "autoloading" PSR: [PSR-0 và PSR-4].
  • Tên Class phải có dạng NameClass.
  • Hằng số trong class phải được khai báo bằng cách viết hoa và chia ra bởi dấu _.
  • Tên của phương thức phải được khai báo theo kiểu camelCase, Ví dụ : functionName, newProject... Tham Khảo thêm tại PSR và Coding Standard in Framgia để có thể xem thêm nhiều thông tin chi tiết về vấn đề này.

Tạm kết

Đến đây, mình xin tạm dừng bài viết về Lập trình hướng đối tượng trong PHP tại đây. Cảm ơn các bạn đã theo dõi. Qua phần 1 và phần 2 này. Hi vọng các bạn đã có một cái nhìn rõ ràng hơn về lập trình hướng đối tượng. Trong phần cuối, mình sẽ viết về các phương pháp thiết kế hướng đối tượng (SOLID). Mong được sự theo dõi và ủng hộ từ các bạn. Xem thêm phần 1Phần 3.