I wanted to get opinions on the "best" way of using class methods in PHP.
I've always believed this is the better way:
class Customer {
public $customer_id;
public $name;
public $email;
public function __construct( $defaults = array() ) {
foreach ( $defaults as $key => $value ) {
if ( property_exists( $this, $key ) ) {
$this->{ $key } = $value;
}
}
}
public function get( $customer_id ) {
$row = ... // Get customer details from database
if ( $row ) {
$this->customer_id = $customer_id;
$this->name = $row['name'];
$this->email = $row['email'];
else {
throw new Exception( 'Could not find customer' );
}
// Don't return anything, everything is set within $this
}
public function update() {
// No arguments required, everything is contained within $this
if ( ! $this->customer_id ) {
throw new Exception( 'No customer to update' );
}
... // Update customer details in the database using
// $this->customer_id,
// $this->name and
// $this->email
}
}
Methodology #1. The class would then be used like this:
$customer = new Customer();
// Get the customer with ID 123
$customer->get( 123 );
echo 'The customer email address was ' . $customer->email;
// Change the customer's email address
$customer->email = 'abc#zyx.com';
$customer->update();
Methodology #2. However, I see most CodeIgniter examples, and WordPress examples, use this type of methodology:
$customer = new Customer();
// Get the customer with ID 123
$cust = $customer->get( 123 );
echo 'The customer email address was ' . $cust->email;
$cust->email = 'abc#zyx.com';
$customer->update( array( 'customer_id' => 123, 'email' => $cust->email ) );
The second example involves the get method returning a copy of the object, and the update method requires arguments passing in rather than working with $this
I can't help feeling the first example is cleaner and better but perhaps I'm overlooking something?
PS: Please ignore the fact the above examples don't use namespaces, setters, getters, etc, I've trimmed them down to the minimum for illustrative purposes.
Related
I'm fairly new to OOP and I'm struggling with setting up my classes. I'm working on a project where I have the following classes: dogTag, dog, user.
This is my current dogTagclass:
class DogTag
{
const POST_TYPE = 'dog-tag';
private ?int $id = null;
private string $number;
private ?Dog $dog;
private ?User $user;
private bool $isActive = false;
private bool $isLost = false;
private string $generationDate;
private ?string $activationDate;
private string $productionStatus;
public function __construct($id = null, string $number, ?Dog $dog = null, ?User $user = null, bool $isActive, bool $isLost, string $generationDate, ?string $activationDate, string $productionStatus)
{
$this->id = $id;
$this->number = $number;
$this->dog = $dog;
$this->user = $user;
$this->isActive = $isActive;
$this->isLost = $isLost;
$this->generationDate = $generationDate;
$this->activationDate = $activationDate;
$this->productionStatus = $productionStatus;
}
Let me explain the functionality of the dog tag. Dog Tags are being generated by a generator and stored into the database. So a dog tag only needs the following arguments when being generated: number (this is a unique generated number and not a unique database ID), isActive, isLost (could be optional), generationDate, productionStatus. All other arguments are not necessarily needed for the generation.
So my first question is: "Do I need to set the optional arguments in the contructor?".
Let me explain why I did this for the moment. When a user receives the unique dog tag number they can activate the dog tag. Therefor I use a method called activateDogTag. For a dog tag to be activated it needs to be attached to a user. Therefor I use the $user argument in the constructor.
Here comes my second question: "Should I use a method setUser(User $user) where I inject the user object into the dogtag class?".
Another problem that I'm struggling with is that I also use an id argument. This will be a bit harder to explain why I'm using this and why it feels wrong.
I'm also using the dogTag class to I can instantiate it with all the data from the database. For the moment I have a static DogTag::get($id) method. This class grabs the data from the database by the ID an instantiates a new DogTag class where I fill all the arguments with the data received from the database.
For information: I'm using WordPress)
public static function get($postId): ?DogTag
{
$post = get_post($postId);
if (empty($post)) {
return null;
}
$dogTagId = $post->ID;
$generationDate = $post->post_date;
$dogTagNumber = get_post_meta($post->ID, 'dog_tag_number', true);
$dogId = get_post_meta($post->ID, 'dog_id', true);
if (empty($dogId)) {
$dog = null;
} else {
$dog = Dog::get($dogId);
}
$userId = $post->post_author;
$user = $userId ? User::get($userId) : null;
$isActive = get_post_meta($dogTagId, 'dog_tag_is_active', true);
$isLost = get_post_meta($dogTagId, 'dog_tag_is_lost', true);
$activationDate = get_post_meta($dogTagId, 'dog_tag_activation_date', true);
$productionStatus = get_post_meta($dogTagId, 'dog_tag_production_status', true);
$dogTag = new self($dogTagId, $dogTagNumber, $dog, $user, $isActive, $isLost,
$generationDate, $activationDate, $productionStatus);
$dogTag->post = get_post($postId);
return $dogTag;
}
This way all the arguments are filled and are available within the class.
So where am I going wrong and what I'm I doing correct? The problem is that I can't really find any solution in my online courses for these problems as they don't go this deep.
Where I'm also struggling is eg with the static activation class (DogTag::activateDogTag):
public static function activateDogTag()
{
// Here goes the form
$dogTag = self::getDogTagByDogTagNumber('4OV9NHOPXLAB6X9B');
// $dogTag = self::getDogTagByDogTagNumber('VBD6JZODTZ6L4YU7');
if (!$dogTag) {
var_dump('no dog tag with this ID found');
}
if(true == $dogTag->getIsActive()) {
var_dump('Dog Tag is already active');
}
if ($dogTag->getUser()) {
var_dump('user is set');
return;
}
var_dump('Activate');
$userId = get_current_user_id();
if (0 === $userId) {
var_dump('not logged in as a user');
}
$dogTag->setIsActive(true);
$dogTag->setUser($userId);
$user = $dogTag->getUser();
var_dump($dogTag);
// Update dog Tag
$args = [
'post_type' => 'dog-tag',
'post_status' => 'publish',
'post_title' => $dogTag->getNumber(),
'post_date' => $dogTag->generationDate,
'post_author' => $dogTag->getUser()->getId(),
'meta_input' => [
'dog_tag_number' => $dogTag->getNumber(),
'dog_tag_is_active' => $dogTag->getIsActive(),
'dog_tag_is_lost' => $dogTag->getIsLost(),
'dog_tag_production_status' => $dogTag->getProductionStatus(),
]
];
var_dump($args);
$postId = wp_update_post($args);
$dogTag->setId($postId);
return $dogTag;
}
Is this a good practice:
$dogTag->setIsActive(true);
$dogTag->setUser($userId);
$user = $dogTag->getUser();
and after the data is stored into the database via wp_update_post by setting the id property via DogTag->setId($postId)?
First of all, if it works then you are not doing anything wrong.
Question 1
It's usually better practice to start with required parameters, followed by optional parameters, if it feels too bloated you could remove all optional parameters from the constructor and create setters for those.
ID/Fetching
What you have here is fine, you could create another class DogTagFactory/DogTagRepository which handles the creation/database process for you if you and reduce bloat.
Overall try not to get too stuck in the chase of "perfect" best practice, it's always going to depend on the situation and usually in the end it does not even matter all that much.
I'm still pretty new with PHPUnit, and I'm trying to figure out how to best test methods as shown in the 3rd class.
I understand how the mock databases work (I think), in that they can return values based on input, from an XML file, etc. I'm not sure how to provide data for the 3rd example, when SQL queries are run inside the methods itself.
I'm trying to test code that accesses info from a DB and performs operations on it. There is no way to feed mocked DB data (e.g., arrays) to these methods currently.
Question: What is the best way to provide data to a method that handles all of its SQL queries internally?
<?php
class ThisMakesSense {
public function checkPassword($original, $hash) {
return (md5($original) == $hash);
}
}
class ThisMakesSenseTest {
public function testCheckPassword() {
$tms = new ThisMakesSense();
$data = array('jdoe#example.com' => 'password1', 'bsmith#example.com' => 'password2');
foraech ($data as $email => $password) {
$hash = $this->mockDB()->doStuff()->getPasswordHashByEmail($email);
$this->assertTrue($tms->checkPassword($password, $hash), "Password for {$email} is invalid");
}
$tms->checkPassword($password, $hash);
}
}
/* The '$this->_db' object is basically an OOP way of using the
* mysql_* /mysqli_* functions. Replacing it is not an option
* right now.
*/
class DontUnderstand {
public function checkPassword($email, $password) {
$this->_db->query("SELECT password_hash FROM users WHERE email = '{$email}'");
$row = $this->_db->fetchAssoc();
return (md5($password) == $row['password_hash']);
}
}
class DontUnderstandTest extends PHPUnit_Framework_TestCase {
public function testCheckPassword() {
$du = new DontUnderstand();
$data = array('jdoe#example.com' => 'password1', 'bsmith#example.com' => 'password2');
foreach ($data as $email => $pass) {
$this->assertTrue($du->checkPassword($email, $pass), "Password for {$email} is invalid");
}
}
}
(To save someone the trouble of commenting, the md5 and query methods are just for a simple example)
I'm not sure what is the best approach, but here's my way. It's based on the assumption of a class that connects internally with a database and a single table. Access is through INSERT, UPDATE, DELETE, SELECT and similar, so no complex JOINs or UNIONs. Another assumption is that I erect the database once (not as part of the testing routine) before I run phpunit. I looked at PHPUnit's Database extension but it appeared to me as too cumbersome to use here, so I quickly mocked this:
class UserProfileTest extends PHPUnit_Framework_TestCase {
protected static
$options,
$dbHandle;
public static function setUpBeforeClass() {
self::$options = array(
'dbHostName' => 'localhost',
'dbUserName' => 'tester',
'dbPassword' => 'pw4tester',
'dbName' => 'test',
'dbTableName' => 'Test',
);
self::$dbHandle = new mysqli( self::$options[ 'dbHostName'], self::$options[ 'dbUserName'], self::$options[ 'dbPassword'], self::$options[ 'dbName'] );
if( self::$dbHandle->connect_errno)
exit( 'Error: No DB connection.');
}
protected function fillDb() {
$query = 'INSERT INTO ' . self::$options[ 'dbTableName'] . ' VALUES (default,"foo","bar",7)';
if( !self::$dbHandle->query( $query) )
exit( 'Error: Could not fill DB.');
}
protected function setUp() {
// always start a TC with empty DB
$query = 'DELETE FROM ' . self::$options[ 'dbTableName'];
if( !self::$dbHandle->query( $query) )
exit( 'Error: Could not empty DB.');
}
// -- test --
public function testGetNumberOfProfiles() {
$profileMgr = new UserProfile( self::$options);
$this->assertEquals( 0, $profileMgr->getNumberOfProfiles() );
$this->fillDb();
$this->assertEquals( 1, $profileMgr->getNumberOfProfiles() );
$this->fillDb();
$this->assertEquals( 2, $profileMgr->getNumberOfProfiles() );
}
So, you connect to the DB when the class is instantiated (setUpBeforeClass), and empty the table before each testcase (in setUp). There is a helper function which inserts a row into the table; it is called when needed.
I have a page dashboard.php, which creates a merchant dashboard that shows deals submitted by the merchant. I'm simply trying to separate types of deals by checking to see if a deal is a suggested deal:
...
while ($deals->have_posts()) : $deals->the_post();
$suggested_deal = SA_Post_Type::get_instance( $post->ID );
$boolsuggesteddeal = $suggested_deal->is_suggested_deal();
...
However, the is_suggested_deal() line is causing the page to not display anything past that line.
The SA_POST_TYPE class is outlined below:
class SA_Post_Type extends Group_Buying_Deal {
...
public static function get_instance( $id = 0 ) {
if ( !$id ) {
return NULL;
}
if ( !isset( self::$instances[$id] ) || !self::$instances[$id] instanceof self ) {
self::$instances[$id] = new self( $id );
}
if ( self::$instances[$id]->post->post_type != parent::POST_TYPE ) {
return NULL;
}
return self::$instances[$id];
}
...
public function is_suggested_deal() {
$term = array_pop( wp_get_object_terms( $this->get_id(), self::TAX ) );
return $term->slug == self::TERM_SLUG;
}
...
Since the class and function are both public, why am I unable to call the function? Any help would be greatly appreciated.
EDIT: I can't figure out how to get error reporting on without showing all site users the errors, I'm on a live site. I tried creating an instance of SA_Post_Type(), but that alone cause the page to fail to load anything after that line.
You have not created an instance of the class, do so like this...
$SA_Post_Type = new SA_Post_Type();
Then you are able to access the function...
$boolsuggesteddeal = $SA_Post_Type->is_suggested_deal();
Since is_suggested_deal is not a static function, you have to create a new instance of the SA_Post_Type class firstly.
$sa_post_type = new SA_Post_Type();
$boolsuggesteddeal = $sa_post_type->is_suggested_deal();
Hope this helps.
for my question on how to use OOP in a beneficial way I assume as an example a BASKET to which its owner (Tom) having a certain ADDRESS (NY) can add ARTICLES (Bike, Car). Finally a BILL is printed containg all these information.
My problem is: How to handle collecting the information desired (here: owner, city, amount of items) from several objects? Because I think it is stupid to do this manually as done below (see 4.), isn't it? (even more since the amount of information increases in reality)
So what is the "clean way" for creating the bill / collecting the information needed in this example?
<?php
$a = new basket('Tom','NY');
$a->add_item("Bike",1.99);
$a->add_item("Car",2.99);
$b = new bill( $a );
$b->do_print();
1.
class basket {
private $owner = "";
private $addr = "";
private $articles = array();
function basket( $name, $city ) {
// Constructor
$this->owner = $name;
$this->addr = new addresse( $city );
}
function add_item( $name, $price ) {
$this->articles[] = new article( $name, $price );
}
function item_count() {
return count($this->articles);
}
function get_owner() {
return $this->owner;
}
function get_addr() {
return $this->addr;
}
}
2.
class addresse {
private $city;
function addresse( $city ) {
// Constructor
$this->city = $city;
}
function get_city() {
return $this->city;
}
}
3.
class article {
private $name = "";
private $price = "";
function article( $n, $p ) {
// Constructor
$this->name = $n;
$this->price = $p;
}
}
4.
class bill {
private $recipient = "";
private $city = "";
private $amount = "";
function bill( $basket_object ) {
$this->recipient = $basket_object->get_owner();
$this->city = $basket_object->get_addr()->get_city();
$this->amount = $basket_object->item_count();
}
function do_print () {
echo "Bill for " . $this->recipient . " living in " . $this->city . " for a total of " . $this->amount . " Items.";
}
}
If you do Tell Dont Ask, you would indeed add a render method to the bill to which you would pass an instance of BillRenderer. Bill would then tell BillRenderer how to render the Bill. This is in accordance with InformationExpert and High Cohesion principles that suggest methods to be on the objects with the most information to fulfill the task.
class Bill
{
…
public function renderAs(BillRenderer $billRenderer)
{
$billRenderer->setRecipient($this->owner);
$billRenderer->setAddress($this->address);
…
return $billRenderer->render();
}
}
BillRenderer (an interface) would then know the output format, e.g. you'd write concrete renderers for PlainText or HTML or PDF:
class TxtBillRenderer implements BillRenderer
{
…
public function render()
{
return sprintf('Invoice for %s, %s', $this->name, $this->address);
}
}
echo $bill->renderAs(new TxtBillRenderer);
If your Bill contains other objects, those would implement a renderAs method as well. The Bill would then pass the renderer down to these objects.
Both basket as well as bill could have a relation to a positions item - an object representing an ordered list of zero or more items with a count and price.
As such a list is an object of it's own it's easy to pass around:
$bill = new Bill($buyer, $address, $basket->getPositions());
However the printing of the bill should be done by the BillPrinter, because it's not the job of the bill to print itself:
$billPrinter = new BillPrinter($bill, $printerDevice);
$billPrinter->print();
First of all , in PHP5 the constructor it public function __construct(). What you are using there is the PHP4 way. And then ther eare other issues with your code:
instead of passing the name of the city to the Basket ( do you mean Cart ?), you should be creating the address object instance and passing it.
do not add items based on name an amount of money to the basket, instead add the whole instance of item, otherwise you will have a lot of problems when switching site language or currency.
the Articles (do you mean Items ?) should be created based on ID, not based on name. The reasons for that are the same as above + you will have issues with uniqueness. And then some of items might have lower price, when bought in combination. You need a way to safely identify them.
As for cleaning up the code there:
you should stop creating instance from given parameters in the constructor. While it is not always a bad thing, in your case you are making a mess there.
Bill should not be responsible for printing itself.
Something like :
class Basket
{
// -- other code
public function handleInvoice( Bill $invoice )
{
$invoice->chargeFor( $this->items );
$invoice->chargeTo( $this->account );
return $invoice->process();
}
}
.. then use it as
$cart = new Basket(..);
// some operation with it
$invoice = new Bill;
$cart->handleInvoice($invoice);
$printer = new PDFPrinter;
// OR new JpegPrinter; OR new FakePrinter OR anything else
$printer->print( $invoice );
This would give you an instance of Bill outside the class which then you can either print or send to someone.
Also , you might benefit from watching the willowing lecture:
Inheritance, Polymorphism, & Testing
Don't Look For Things!
Clean Code I: Arguments
Hello and thanks for being there,
I would like to pass a variable ($user) from a previous function to another one, but I need to use the arguments of the new function to pass the values that will render this new one.
Is there any way I can pass a variable from another function to a new function that only expects three arguments, and none of them is the variable from the previous function?
Example:
function my_function($country, $age, $colour) {
if ($user = true) {
echo "User is from " . $country . " and " . $age . " and his favourite colour is " . $colour;
}
}
my_function("italy", 19, "red");
It works if I put inside function my_function:
global $user;
but I believe using global variables is not a good practice.
Any idea on how to pass it as an argument? Should I just add it as another variable after $colour in the arguments of the new function?
Thanks a lot for your help :)
You can pass it as an argument, or better do this:
if ($user) my_function("italy", 19, "red");
since you don't have to use the $user variable inside that function.
You can use this function but best practice will be using class.
i .e if you call my_function("italy", 19, "red"), $user will be false by default
function my_function($country, $age, $colour, $user=false) {
if ($user == true) {
echo "User is from " $country . "and " . $age . " and his favourite colour is " . $colour;
}
}
my_function("italy", 19, "red",true);
Well, global variables are a bad practice, but a viable one in your case.
I would definitely recommend you looking into/using a Object Oriented approached.
There are really a bunch of different ways to achieve what your trying.
One way would be to encapsulate your code into a object.
<?php
class My_Cool_Class {
public $user = false;
public function myFunction($country, $age, $color) {
if ($this->user)
echo "User is from {$country} and {$age} years old and his favourite colour is {$color}";
}
}
$class = new My_Cool_Class();
$class->user = new User();
$class->myFunction("Italy", 19, "red");
Or you could implement a Singleton Pattern to easily get access to your object/functions from anywhere.
<?php
class My_Cool_Class {
public $user = false;
protected static $_instance;
public function getIntance() {
if(!self::$_instance)
self::$_instance = new self();
return self::$_instance;
}
public function setUser($user) {
$this->user = $user;
}
public function myFunction($country, $age, $color) {
if ($this->user)
echo "User is from {$country} and {$age} years old and his favourite colour is {$color}";
}
}
//Set User from anywhere with this
My_Cool_Class::getInstance()->setUser($user);
//Call your function anywhere with this.
My_Cool_Class::getInstance()->myFunction("Italy", 19, "red");
If your previous function is something like this:
/**
* Callback function for so_user_data() that tells if we want to give you info about the user or not.
* #param (string) $user | Accepts a Username as input
* #return (boolean) | true if the User is 'Rob'
*/
function so_user_fn( $user )
{
$allowed_users = array(
'Rob'
,'Jay'
,'Silent Bob'
);
if ( in_array $user, $allowed_users ) )
return true;
// false if not 'Rob'
return false;
}
/**
* Shows the country, age & fav. colour of a user or denies displaying this information
* if the user is not a public v.i.p. or someone other we want to give you info about.
* #uses so_user_fn() | used to determine if the user is 'Rob'
* #param (string) $user | Username
* #param (string) $country | (optional) Country of origin
* #param (integer) $age | (optional) Age of the user
* #param (string) $colour | (optional) Fav. Colour
*/
function so_user_data( $user, $country = 'an unknown country', $age = 'unknown', $colour = 'not known' )
{
$output = "$user is from {$country} and {$age} years old. His favourite colour is {$colour}.";
// Only print the output if the user is 'Rob'
if ( so_user_test( $user ) )
return print $output;
return print 'We are not allowed to give you information about this user.';
}
You can call it like this:
so_user_data( 'Fancy Franzy' );
// outputs: We are not allowed to give you information about this user.
so_user_data( 'Rob' );
// outputs: Rob is from an unknown country and unknown years old. His favourite colour is not known.
so_user_data( 'Silent Bob', 'U.S.A.', '38', 'brown' );
// outputs: Silent Bob is from U.S.A. and 38 years old. His favourite colour is brown.