Framework Extensibility¶
This article summarizes the current possibilities of the framework extension, provides a list of customizations that are not achievable now but are planned to be enabled soon, as well as a list of customizations that are not (and will not be) possible at all.
What is achievable easily¶
- Extending an entity
- Adding a new attribute
- Note: There are some limitations when extending OrderItem. For more see the documentation
- The administration can be extended by:
- Adding a new administration page along with the side menu and breadcrumbs
- Extending particular forms without the need of the template overriding
- Customizing database migrations
- Adding a new migration as well as skipping and reordering the existing ones
- Configuring the smoke tests (see
RouteConfigCustomization
class)- Note: This is now achievable as the configuration class is located in the open box project-base. However, that makes upgrading the component harder, so the configuration is planned to be re-worked.
- Implementing custom product feed or modifying an existing one
- Implementing a basic data import to import data to you e-shop from an external source
- Adding a new cron module and configuring it
- Extending the application using standard Symfony techniques
- E.g. overriding Twig templates, routes, services, ...
- Adding a new advert position to be used in the administration section Marketing > Advertising system
- Open-box modifications in
project-base
- E.g. adding new entities, changing the FE design, customization of FE javascripts, adding new FE pages (routes and controllers), ...
- Hiding the existing features and functionality
- You can read Npm and webpack to know how to extend javascript
What is achievable with additional effort¶
- Extending factories and controllers - see the commit in demoshop
- Adding form option into existing form - see the commit in demoshop
- Extending administration form theme - see commit in demoshop
- Changing an entity association - see commit in demoshop and actual association change
- This change is complicated and potentially dangerous
Which issues are going to be addressed soon¶
- Extending data fixtures (including performance data fixtures)
- Extending data grids in the administration
- Extending classes like Repositories without the need for changing the project-base tests
What is not supported¶
- Removing an attribute from a framework entity
- Changing a data type of an entity attribute
- Removing existing entities and features
- Extending the
Money
class and closely related classes (e.g.,MoneyType
)
Examples of implemented features on the Demoshop repository¶
- Shipping method with pickup places
- new shipping method Zasilkovna
- pick up places are downloaded by cron
- order process change
- details in a issue description
- Product attribute "condition"
- product entity extension
- administration form extension
- frontend product change
- google feed change
- detailed info in a issue description
- Second description of a category
- category entity extension
- administration form extension
- new multidomain
- frontend product list change
- detailed info in a issue description
- Twig templates cache
- performance improved by ~15%
- cache is invalidated every 5 minutes
- Hidden the functionality of the flags
- hidden functionality in administration
- hidden functionality in frontend
- flags do not affect eshop at all
- Company account with multiple users
- group user accounts under one company account
- separate users login credentials
- share company attributes
- change association from 1:1 to 1:N
Making the static analysis understand the extended code¶
Problem 1¶
When extending framework classes, it may happen that tools for static analysis (e.g., PHPStan, PHPStorm) will not understand your code properly. Imagine this situation:
- You have a controller that is dependent on a framework service:
namespace App\Controller\Front;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade;
class ProductController
{
/**
* @var \Shopsys\FrameworkBundle\Model\Product\ProductFacade
*/
protected $productFacade;
/**
* @param \Shopsys\FrameworkBundle\Model\Product\ProductFacade $productFacade
*/
public function __construct(ProductFacade $productFacade)
{
$this->productFacade = $productFacade;
}
}
- In your project, you extend the framework's
ProductFacade
service:
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade as BaseProductFacade;
class ProductFacade extends BaseProductFacade
{
public function myCustomAwesomeFunction()
{
return 42;
}
}
- You register your extension in DI services configuration and thanks to that, your class is used in
ProductController
instead of the one fromFrameworkBundle
, so far so good:
Shopsys\FrameworkBundle\Model\Product\ProductFacade: '@App\Model\Product\ProductFacade'
However, when you want to use your myCustomAwesomeFunction()
in ProductController
, the static analysis is not aware of that function.
Solution¶
To fix this, you need to change the annotations properly:
class ProductController
{
/**
- * @var \Shopsys\FrameworkBundle\Model\Product\ProductFacade
+ * @var \App\Model\Product\ProductFacade
*/
protected $productFacade;
/**
- * @param \Shopsys\FrameworkBundle\Model\Product\ProductFacade $productFacade
+ * @param \App\Model\Product\ProductFacade $productFacade
*/
public function __construct(ProductFacade $productFacade)
{
$this->productFacade = $productFacade;
}
}
Luckily, you do need to fix the annotations manually. There is the Phing target annotations-fix
, that handles everything for you.
Problem 2¶
There might be yet another problem with static analysis when extending framework classes. Imagine the following situation:
- In the framework, there is
ProductFacade
that hasProductRepository
property
namespace Shopsys\FrameworkBundle\Model\Product;
class ProductFacade
{
/**
* @var \Shopsys\FrameworkBundle\Model\Product\ProductRepository
*/
protected $productRepository;
/**
* @return \Shopsys\FrameworkBundle\Model\Product\ProductRepository
*/
public function getProductRepository()
{
retrun $this->productRepository;
}
}
- In your project, you extend
ProductRepository
andProductFacade
as well. - Then, in your extended facade, you want to access the repository (generally speaking, you want to access the parent's property that has a type that is extended in your project, or you want to access a method that returns a type that is already extended):
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade as BaseProductFacade;
class ProductFacade extends BaseProductFacade
{
public function myCustomAwesomeFunction()
{
$this->productRepository; // static analysis thinks this is of type \Shopsys\FrameworkBundle\Model\Product\ProductRepository
$this->getProductRepository(); // static analysis thinks this is of type \Shopsys\FrameworkBundle\Model\Product\ProductRepository
}
}
- Once again, static analysis is not aware of the extension.
Solution¶
You don't need to override the method or property to fix this. You just need to add proper @method
and @property
annotations to your class:
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade as BaseProductFacade;
+ /**
+ * @method \App\Model\Product\ProductRepository getProductRepository()
+ * @property \App\Model\Product\ProductRepository $productRepository
+ */
class ProductFacade extends BaseProductFacade
{
Even this scenario is covered by annotations-fix
phing target.
Problem 3¶
One kind of problem is not fixed automatically and needs to be addressed manually.
Shopsys Platform uses a kind of magic for working with extended entities (see EntityNameResolver
class),
and static analysis tools are not aware of that fact.
Imagine the following situation:
- You have extended
Product
entity in your project - In the framework, there is
ProductFacade
class that is not extended in your project, and it has a method that returns instances ofProduct
entity (in fact, it returns instances of your childProduct
entity thanks to the mentionedEntityNameResolver
magic).
namespace Shopsys\FrameworkBundle\Model\Product;
// the class has no extension in your project
class ProductFacade
{
/**
* This class is not extended in the project either
* @var \Shopsys\FrameworkBundle\Model\Product\ProductRepository
*/
protected $productRepository;
/**
* @return \Shopsys\FrameworkBundle\Model\Product\Product
*/
public function getById($id)
{
// despite the annotation, extended Product entity from your project is returned
return $this->productRepository->getById($id);
}
}
- You have a controller that is dependent on the framework service:
namespace App\Controller\Front;
use Shopsys\FrameworkBundle\Model\Product\ProductFacade;
class ProductController
{
/**
* @var \Shopsys\FrameworkBundle\Model\Product\ProductFacade
*/
protected $productFacade;
/**
* @return \Shopsys\FrameworkBundle\Model\Product\ProductFacade
*/
public function __construct(ProductFacade $productFacade)
{
return $this->productFacade = $productFacade;
}
/**
* @param int $id
* Your Product instance is returned indeed, but static analysis is confused
* @return \App\Model\Product\Product
*/
private function myAwesomeMethod($id)
{
return $this->productFacade->getById($id);
}
}
In such a case, the static analysis does not understand that the extended Product
entity is returned.
Solution¶
This needs to be fixed manually using a local variable with an inline annotation:
private function myAwesomeMethod($id)
{
+ /** @var \App\Model\Product\Product $product */
+ $product = $this->productFacade->getById($id);
- return $this->productFacade->getById($id);
+ return $product;
}
As a workaround, you can create an empty class extending the one from the framework, register the extension in your services.yaml
, and then use php phing annotations-fix
to fix appropriate annotations for you.
Which way to go really depends on your situation. If you are likely to extend the given framework class sooner or later, or the same problem with the class is reported in many places, it would be better to create the empty extended class right away. Otherwise, it might be better just extracting and annotating the variable manually (like in this commit in monorepo) as it is quicker, and you can avoid having an unused empty class in your project.
Tip¶
If you are a fan of an automation and PHPStorm user at the same time, you can simplify things even more and set your IDE to automatically run the phing target every time you e.g., change something in your project. This can be achieved by setting up a custom "File watcher".