Configurable products price class
General concept

From your explanation it seems to me that, apart from dimensions and prices, different categories of products may have different additional features (like glasstype, hinges, and coating in your showerdoor example).

But since you showed only one product, I can't be certain. So What follows is based on the hypothesis that:


there is potentially a number of these features and each category of product uses only some of them, but each feature may be shared by several categories.
for a given feature, the way it is processed is the same for all involved categories of products.


That's said, I think you'd better to write all these methods in your abstract class, so they're always already available for each new derived class you create for a kind of product.

More, I think that even if certain features are specific to certain unique categories (what you called "exceptions"), they also should be written in the abstract class.

At the same time, in order to keep the whole process secure (i.e. be sure that a derived class can't accept features that don't regard their category), each derived class could have its own allowed-features list.

Applying the above strategy leads to something like this for the abstract class:

abstract class ConfigurableProduct {

  public $squareMeters;
  // Stores the prices
  protected $_price = array();

  // Initializes object
  public function __construct($data = [], $allowed_features = []) {
    // Set square meters
    $this-&gt;squareMeters = $data['square_meters'];

    // Set features when they are both allowed and present
    foreach ($allowed_features as $feature) {
      if (isset($data[$feature])) {
        $method = 'set' . $feature;
        $this-&gt;$method($data($feature));
      }
    }
  }

  // Adds price to _price array   
  public function addPrice( $value ) {
    $this-&gt;_price[] = $value;
  }

  // Sums prices in _price array
  public function getPrice() {
    return array_sum( $this-&gt;_price );  
  }

  // One example of feature method for "someFeature"
  public function setSomeFeature($id) {
    // Check for part id (so integer)
    if (isset($id) &amp;&amp; is_int($id)) {
      // Calculate price
      $value = gtp_wizard_part_price($id) * $this-&gt;squareMeters;
      // Add price to price array
      $this-&gt;addPrice( $value );
    }
  }

  // Other feature methods here...
}


And something like this for a category of product:

class Showerdoor extends ConfigurableProduct {

  protected $allowed_features = [
    'glasstype',
    'coating',
    'hinges',
  ];

  // Initializes object
  public function __construct($data) {
    // Parent constructor
    parent::__construct($data, $this-&gt;allowed_features);
  }
}


At my eyes this way ensures both consistency and simplicity (hence readability): all existing feature methods are gathered at a unique place, once for all; granting a feature to a category is only matter of simple registration.

Some minor aspects

I'm a bit surprised you calculate square meters outside of the object: unless I'm missing something subtle, it could be done (again, once for all) in the abstract class, so the __construct() function becomes:

  public function __construct($data = [], $allowed_features = []) {
    // Set square meters
    $this-&gt;squareMeters = $data['width'] * $data['height'] / 1000000;
    // ...


And the object creation is only:

// Parts (ids from database)
$glasstype      = 282236;
$hinges         = 282242;
$coating        = 282296;

// Create object
$showerdoor = new Showerdoor(
   array( 
    'width'     =&gt; 800,
    'height'    =&gt; 2000,
    'glasstype' =&gt; $glasstype, 
    'hinges'    =&gt; $hinges,
    'coating'   =&gt; $coating,
  ) 
);


Also I'd suggest adding the availibility for the app to access data in the object, avoiding to keep global variables outside of it.
This way, the abstract class looks like this:

abstract class ConfigurableProduct {

  public $squareMeters;
  public $data;
  // Stores the prices
  protected $_price = array();

  // Initializes object
  public function __construct($data = [], $allowed_features = []) {
    // Set square meters
    $this-&gt;squareMeters = $data['width'] * $data['height'] / 1000000;
    // Keep a copy of data
    $this-&gt;data = $data;
    //...


And the object creation is even simpler:

// Create object
$showerdoor = new Showerdoor(
   array( 
    'width'     =&gt; 800,
    'height'    =&gt; 2000,
    'glasstype' =&gt; 282236, 
    'hinges'    =&gt; 282236,
    'coating'   =&gt; 282296,
  ) 
);


Regarding the feature methods you gave in example, note that setGlasstype() and setCoating() are perfectly identical and hence can be factorized.
Obviously it can be achieve by simply invoking a common (protected) method from each of them. But if this situation is frequent you might use another strategy, using aliases, like this:

  // Aliases for feature methods
  protected $feature_aliases = [
    'glasstype'   =&gt; 'GlassType_setCoating',
    'setcoating'  =&gt; 'GlassType_setCoating',
    'sethinges'   =&gt; 'SetHinges',
  ];


So the __construct() function must be adapted:

// Set features when they are both allowed and present
foreach ($allowed_features as $feature) {
  if (isset($data[$feature])) {
    $method = 'set' . $this-&gt;feature_aliases[$feature];
    $this-&gt;$method($data($feature));
  }
}


This way, again, we get more consistency and readability, since there is a clear and brief registry for all existing feature methods.
Note that this also simply solves an issue I didn't pay attention in the above: the difference between all-lower and camelcase feature names.

Hope this helps!