Jump to content

(OOP) Approaches to using different code in a Page Class method depending on page name?


Recommended Posts

Let's say I have the following pages with these names that use the "company" template:

  • microsoft
  • apple
  • google

Let's say my page class has a method called 'foo'.  I want the code that's run to be dependent on the page name, but I don't want to do it like this:

public function foo() {
	if($this->name=="microsoft") {
		// code for microsoft
	} elseif($this->name=="apple") {
		// code for apple
 	} elseif($this->name=="google") {
		// code for google
	}
}

Instead, I want each company to have their own class (but keep the pages as 'company' class -- don't touch!) with each method implemented within that class.

How would you go about this?

I solved this by adding a property to the Page class using property hooks which has access to the respective method (what's this technique called? is there a name for it?), but wondering if there's a better way:

class MicrosoftCompanyTools {
  public function foo() {
    echo "test";
  }
}

wire()->addHookProperty('CompanyPage::tools', function($event) {
  $company = $event->object;
  $companyClassName = "ProcessWire\\" . ucfirst(str_replace('-', '', $company->name))."CompanyTools";
  if(!class_exists($companyClassName)) $event->return = false;
  $event->return = new $companyClassName();
});

// example call
$pages->get('template=company,name=microsoft')->tools->foo();
Link to comment
Share on other sites

 Hi,

To be clean I wouldn't use the page name. Name purpose is essentially for url management, not for "company"'s one. 🙂 Name can change in the future, but the company associated with the page won't.
So I would create, maybe a select options with company names and ID. Or if these tools have properties that need to be stored in PW fields, I would create a tools template and tools page references on the company page.

For the code part (using a select options), I don't like using hooks when it's not necessary, hooks make code less clear and harder to maintain on long term. So my approach is like good old classic and verbose OOP. 😄 I wrote this on a simple text editor, so maybe mistakes...

/**
* Values of this enum must be also set in select options values.
*/
enum CompanyToolsType:int {
  
  case MICROSOFT = 1;
  case GOOGLE = 2;
}

interface CompanyTools {

  public function foo():void;
}

class MicrosoftTools implements CompanyTools {

  public function foo():void {
    echo "I'm a Microsoft tool";
  }
}

class GoogleTools implements CompanyTools {

  public function foo():void {
    echo "I'm a Google tool";
  }
}

class CompanyToolsFactory {

  public static function getTools(CompanyToolsType $type):CompanyTools {
    
    return match($type) {
      
      CompanyToolsType::MICROSOFT => new MicrosoftTools(),
      CompanyToolsType::GOOGLE => new GoogleTools(),
    };
  }
}

/**
* @property $companyTypeSelect I don't remember what type it is (Select Options field with single selected value)
*/
class CompanyPage extends Page {

  public function getTools():CompanyTools {
  
    return CompanyToolsFactory::getTools($this->getToolsType());
  }
  
  
  public function getToolsType():CompanyToolsType {
    
    // TODO Can throw ValueError
    return CompanyToolsType::from(intval($this->companyTypeSelect->value));
  }
}

 

Edited by da²
  • Like 2
Link to comment
Share on other sites

And in case using page names is mandatory, the changes are:

/**
* Values of this enum must be the same as company pages names.
*/
enum CompanyToolsType:string {
  
  case MICROSOFT = "microsoft";
  case GOOGLE = "google";
}

class CompanyPage extends Page {

  //public function getTools...
  
  public function getToolsType():CompanyToolsType {
    
    // TODO Can throw ValueError
    return CompanyToolsType::from($this->name);
  }
}

 

You can also remove the enum and ask the factory directly with a string, but enum adds a layer of error checking.
Same for factory, it can be removed and code moved to CompanyPage, the factory primarily helps with better separation of responsibilities.

Edited by da²
  • Like 1
Link to comment
Share on other sites

As @da² said, you shouldn't rely on page names like that in your code since names can change and also, you shouldn't prevent that (for SEO, localization and other reasons).

The next best thing is to use different templates for each of your companies. With that, you get custom page classes for each of them and your challenge solves itself.

Doubling down on your code though, why not use a new method in your custom page class, e.g.

<?php namespace ProcessWire;

class CompanyPage extends Page {
  // [...]
  
  public function tools() {
    $companyClassName = "ProcessWire\\" . ucfirst(str_replace('-', '', $this->name))."CompanyTools";
    if(!class_exists($companyClassName)) return false;
    $event->return = new $companyClassName();
  }
  
  // [...]
}

instead of the hook? You can also go all in on the PW style using hooks:

<?php namespace ProcessWire;

wire()->addHookProperty('CompanyPage(name=microsoft)::tools', function($event) { $event->return = new MicrosoftCompanyTools(); });
wire()->addHookProperty('CompanyPage(name=google)::tools', function($event) { $event->return = new GoogleCompanyTools(); });
wire()->addHookProperty('CompanyPage(name=apple)::tools', function($event) { $event->return = new AppleCompanyTools(); });
// [...]

.. but this removes some of the flexibility of the class_exists call above.

Edited by poljpocket
fix hooks
Link to comment
Share on other sites

Thanks for the replies.

I agree that using the page name has a bit of an "ick" factor, but the name won't change and I can lock the field regardless.

I am purposely trying not to create a new specific template for each company, and although that approach is probably more clean and direct, it messes with the following extra part...

32 minutes ago, poljpocket said:

Doubling down on your code though, why not use a new method in your custom page class, e.g.

I didn't mention in my example that there is also a "Company Types" field where a company can have multiple selections of the following:

  • Provider
  • Fulfiller
  • Biller

If the company is a "Provider", then I want the tools method (I've already programmed this but left it out in my example code to keep things simple).  Although that could lead to some issues if I tried accessing that method and it's not available unless I did some extra checks, but that's not totally clean as well.

Link to comment
Share on other sites

1 hour ago, Jonathan Lahijani said:

If the company is a "Provider", then I want the tools method

So not all company pages have tools? Why not adding a method CompanyPage::hasTools():bool?

if ($companyPage->hasTools()) $companyPage->getTools()->foo();

  

1 hour ago, Jonathan Lahijani said:

Although that could lead to some issues if I tried accessing that method and it's not available

Yes, methods should always be available.

Edited by da²
Link to comment
Share on other sites

In this case, you can use this hook which gets rid of any complicated logic:

<?php namespace ProcessWire;

wire()->addHookProperty('Page(template=company,company_types=provider)::tools', function($event) { $event->return = new ProviderCompanyTools(); });

 

Edited by poljpocket
Link to comment
Share on other sites

13 hours ago, Jonathan Lahijani said:
wire()->addHookProperty('CompanyPage::tools', function($event) {
  $company = $event->object;
  $companyClassName = "ProcessWire\\" . ucfirst(str_replace('-', '', $company->name))."CompanyTools";
  if(!class_exists($companyClassName)) $event->return = false;
  $event->return = new $companyClassName();
});

// example call
$pages->get('template=company,name=microsoft')->tools->foo();

I think this is perfectly fine, but why not add this as a method in your class to get rid of the hook?

// in your pageclass
public function tools() {
	// get tools based on page name
	$toolsClass = ...
	// return instance of tools
	return new $toolsClass();
}

// usage
$pages->get(...)->tools()->foo();

The benefit is that your IDE will understand your code and if you add return types etc. it will help you with code completion.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...