It looks to me you're creating the different classes only for the display parameter of the DOMElement#id. Whether or not that makes sense (lets keep preference out of the answer), what you have is a 1:1
relationship between the DOMElement#id
and each concrete ProductType
.
This relationship is abstract, therefore you can use the abstract base-class already to encode the name of the type with the class:
abstract class ProductType
{
private function typeName(): string
{
$pathname = $this::class;
$basename = ltrim(substr($pathname, (int)strrchr($pathname, '\\')), '\\');
if (!str_ends_with($basename, 'Type')) {
throw new BadMethodCallException('Hierarchy error');
}
return strtolower(substr($basename, 0, -4));
}
final public function displayInputs() : ?string
{
try {
$idString = json_encode(sprintf('%s-div', $this->typeName()), JSON_THROW_ON_ERROR);
} catch (BadMethodCallException|JsonException) {
return null;
}
return <<<HTML
<script>document.getElementById($idString).style.display = "block";</script>
HTML;
}
}
This example also already finally implements the abstract ProductType::displayInputs()
method as all classes that are a ProductType use the same code (template):
class BookType extends ProductType {}
class DVDType extends ProductType {}
class TableType extends ProductType {}
You can then use that 1:1
relationship to map the value of the <select>
to a concrete ProductType
.
<form>
<select name="ProductType">
<option>Book</option>
<option>DVD</option>
<option>Table</option>
</select>
<button>
</form>
The HTML with its tags already represents the actual hierarchy well. All you need is to have your ProductType to also provide the option:
abstract class ProductType
{
...
public static function option(string $ProductType): self
{
try {
return new("{$ProductType}Type");
} catch (Throwable) {
throw new InvalidArgumentException("Name error: '$ProductType'");
}
}
...
This is already functional:
$_GET['ProductType'] = 'DVD';
var_dump(ProductType::option(...$_GET));
object(DVDType)#1 (0) {
}
And there are no more conditionals regarding the sub-types.
Now to turn this static class hierarchy into something polymorphic, you have to give it an interface:
interface Product
{} # intentionally left empty
and make the concrete classes implement that interface:
abstract class ProductType implements Product
{
...
and make the abstract creation method adhere to the interface, not the abstract base-type:
...
public static function option(string $ProductType): Product # was: self
As this is a bare-minimum implementation of polymorphism by your question, the interface can remain empty up to this point as there is a single type only, the product, and all implementations that exist are actually 100% from that of a single template as well, and therefore this could be pretty close to a single implementation only as well.
There is not much more use, but having the interface for the future (it enables polymorphism).
A real-life example would perhaps start with the interfaces and create systems delegating the work across the different layers (MVC and all the different parts of a software).
This is normally not necessary with PHP (as it does not need to be compiled), but if you'd like (or must), I'd start with the interfaces, loader- and unit-tests first as PHP code isn't compiled and needs to be run for verification (does the whole code across different files load and compute?).
There is another benefit with early testing: If you start to create the design without tests early on, it is easy to often walk into a wrong direction until noticed, so early testing reveals interface and methods more easily.