I am experiencing an issue while trying to disable a Magento2 module causing custom values to still be visible in the customer edit page.
I would like to know what i have to do to completely get rid of a module and its data from a Magento2 system.
Magento version: 2.3.2
PHP version: 7.2.19
A custom(own) Magento2 module was installed by:
Copying code: app/code/VENDOR/MODULE
Running: magento module:enable VENDOR_MODULE
Running magento setup:upgrade
This module creates a couple of Customer EAV attributes that correctly show in the customer edit form.
I am able to populate/save/update values successfully.
I am disabling the module as such:
Running: magento module:disable VENDOR_MODULE
Running magento setup:upgrade
Completely removing the app/code/VENDOR/MODULE directory
When i navigate back to the customer edit page i can still see the attributes, visible and populated with previously entered data.
At this point i have tried the following:
Manually removing the entry in setup_module.
Including a Uninstall class.
A combination of magento cache:clean && magento setup:di:compile.
Classes attached:
InstallData.php
namespace VENDOR\MODULE\Setup;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Customer\Model\Customer;
use Magento\Customer\Setup\CustomerSetupFactory;
class InstallData implements InstallDataInterface {
private $customerSetupFactory;
/**
* Constructor
*
* #param \Magento\Customer\Setup\CustomerSetupFactory $customerSetupFactory
*/
public function __construct(CustomerSetupFactory $customerSetupFactory) {
$this->customerSetupFactory = $customerSetupFactory;
}
/**
* {#inheritdoc}
*/
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) {
$this->installModule1($setup, $context);
$this->installModule2($setup, $context);
}
private function installModule1(ModuleDataSetupInterface $setup, ModuleContextInterface $context) {
$customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);
$customerSetup->addAttribute(\Magento\Customer\Model\Customer::ENTITY, 'module1', [
'type' => 'varchar',
'label' => 'Module1 label',
'input' => 'text',
'source' => '',
'required' => false,
'visible' => true,
'position' => 500,
'system' => false,
'backend' => ''
]);
$attribute = $customerSetup->getEavConfig()->getAttribute('customer', 'module1')
->addData(['used_in_forms' => [
'adminhtml_customer',
'adminhtml_checkout',
'customer_account_create',
'customer_account_edit'
]
]);
$attribute->save();
}
private function installModule1(ModuleDataSetupInterface $setup, ModuleContextInterface $context) {
$customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);
$customerSetup->addAttribute(\Magento\Customer\Model\Customer::ENTITY, 'module2', [
'type' => 'varchar',
'label' => 'Module2 label',
'input' => 'text',
'source' => '',
'required' => false,
'visible' => true,
'position' => 500,
'system' => false,
'backend' => ''
]);
$attribute = $customerSetup->getEavConfig()->getAttribute('customer', 'module2')
->addData(['used_in_forms' => [
'adminhtml_customer',
'adminhtml_checkout',
'customer_account_create',
'customer_account_edit'
]
]);
$attribute->save();
}
}
Uninstall.php
namespace VENDOR\MODULE\Setup;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Db\Select;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\Setup\UninstallInterface;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class Uninstall implements UninstallInterface {
private $_eavSetupFactory;
private $_mDSetup;
public function __construct(EavSetupFactory $eavSetupFactory, ModuleDataSetupInterface $mDSetup) {
$this->_eavSetupFactory = $eavSetupFactory;
$this->_mDSetup = $mDSetup;
}
public function uninstall(SchemaSetupInterface $setup, ModuleContextInterface $context) {
$installer = $setup;
$eavSetup = $this->_eavSetupFactory->create(['setup' => $this->_mDSetup]);
$eavSetup->removeAttribute(\Magento\Catalog\Model\Customer::ENTITY, 'module1');
$eavSetup->removeAttribute(\Magento\Catalog\Model\Customer::ENTITY, 'module2');
}
}
For Customer attributes you need to delete the specific attributes entry from table "eav_attribute" you can search by "attribute_code" and delete that row, you have to delete attributes from the database because there is no functionality in admin to delete an attribute
This is the method by which the custom attribute can be removed as I also tried manually to delete that module attribute and it took more than 1 day to find this solution.
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$eavSetup->removeAttribute(Customer::ENTITY, "<attribute name>");
}
You can remove multiple attribute too at same time by separating attribute name by commas.
After this just run -
bin/magento setup:upgrade && bin/magento setup:static-content:deploy -f
Related
I'm testing a first simple version for a Multistore-compatible module. It has just two settings, which have to be saved differently depending on the current shop Context (a single shop mainly).
Now, I know that from 1.7.8 there are additional checkbox for each setting in the BO Form, but I have to manage to get it work also for 1.7.7.
Now, both Configuration::updateValue() and Configuration::get() should be multistore-ready, meaning that they update or retrieve the value only for the current context, so it should be fine.
The weird thing is that, after installing the test module, if I go to the configuration page, it automatically redirects to an All-Shop context and, if I try to manually switch (from the dropdown in the top right), it shows a blank page. Same thing happens if I try to deactivate the bottom checkbox "Activate this module in the context of: all shops".
Here is my code:
class TestModule extends Module
{
public function __construct()
{
$this->name = 'testmodule';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'Test';
$this->need_instance = 1;
$this->ps_versions_compliancy = [
'min' => '1.7.0.0',
'max' => '1.7.8.0',
];
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l("Test Module");
$this->description = $this->l('Collection of custom test extensions');
$this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
if (!Configuration::get('TESTM_v')) {
$this->warning = $this->l('No version provided');
}
}
public function install()
{
if (Shop::isFeatureActive()) {
Shop::setContext(Shop::CONTEXT_ALL);
}
return (
parent::install()
&& $this->registerHook('header')
&& $this->registerHook('backOfficeHeader')
&& Configuration::updateValue('TESTM_v', $this->version)
);
}
public function uninstall()
{
if (Shop::isFeatureActive()) {
Shop::setContext(Shop::CONTEXT_ALL);
}
return (
parent::uninstall()
&& $this->unregisterHook('header')
&& $this->unregisterHook('backOfficeHeader')
&& Configuration::deleteByName('TESTM_v')
);
}
public function getContent()
{
// this part is executed only when the form is submitted
if (Tools::isSubmit('submit' . $this->name)) {
// retrieve the value set by the user
$configValue1 = (string) Tools::getValue('TESTM_CONFIG_1');
$configValue2 = (string) Tools::getValue('TESTM_CONFIG_2');
// check that the value 1 is valid
if (empty($configValue1)) {
// invalid value, show an error
$output = $this->displayError($this->l('Invalid Configuration value'));
} else {
// value is ok, update it and display a confirmation message
Configuration::updateValue('TESTM_CONFIG_1', $configValue1);
$output = $this->displayConfirmation($this->l('Settings updated'));
}
// check that the value 2 is valid
Configuration::updateValue('TESTM_CONFIG_2', $configValue2);
$output = $this->displayConfirmation($this->l('Settings updated'));
}
// display any message, then the form
return $output . $this->displayForm();
}
public function displayForm()
{
// Init Fields form array
$form = [
'form' => [
'legend' => [
'title' => $this->l('Settings'),
],
'input' => [
[
'type' => 'text',
'label' => $this->l('Custom CSS file-name.'),
'name' => 'TESTM_CONFIG_1',
'size' => 20,
'required' => true,
],
[
'type' => 'switch',
'label' => $this->l('Enable custom CSS loading.'),
'name' => 'TESTM_CONFIG_2',
'is_bool' => true,
'desc' => $this->l('required'),
'values' => array(
array(
'id' => 'sw1_on',
'value' => 1,
'label' => $this->l('Enabled')
),
array(
'id' => 'sw1_off',
'value' => 0,
'label' => $this->l('Disabled')
)
)
],
],
'submit' => [
'title' => $this->l('Save'),
'class' => 'btn btn-default pull-right',
],
],
];
$helper = new HelperForm();
// Module, token and currentIndex
$helper->table = $this->table;
$helper->name_controller = $this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->currentIndex = AdminController::$currentIndex . '&' . http_build_query(['configure' => $this->name]);
$helper->submit_action = 'submit' . $this->name;
// Default language
$helper->default_form_language = (int) Configuration::get('PS_LANG_DEFAULT');
// Load current value into the form or take default
$helper->fields_value['TESTM_CONFIG_1'] = Tools::getValue('TESTM_CONFIG_1', Configuration::get('TESTM_CONFIG_1'));
$helper->fields_value['TESTM_CONFIG_2'] = Tools::getValue('TESTM_CONFIG_2', Configuration::get('TESTM_CONFIG_2'));
return $helper->generateForm([$form]);
}
/**
* Custom CSS & JavaScript Hook for FO
*/
public function hookHeader()
{
//$this->context->controller->addJS($this->_path.'/views/js/front.js');
if (Configuration::get('TESTM_CONFIG_2') == 1) {
$this->context->controller->addCSS($this->_path.'/views/css/'.((string)Configuration::get('TESTM_CONFIG_1')));
} else {
$this->context->controller->removeCSS($this->_path.'/views/css/'.((string)Configuration::get('TESTM_CONFIG_1')));
}
}
}
As you can see it's a pretty simple setting: just load a custom CSS file and choose if loading it or not. I've red official PS Docs per Multistore handling and searched online, but cannot find an answer to this specific problem.
I've also tried to add:
if (Shop::isFeatureActive()) {
$currentIdShop = Shop::getContextShopID();
Shop::setContext(Shop::CONTEXT_SHOP, $currentIdShop);
}
To the 'displayForm()' function, but without results.
Thank you in advance.
It seemsit was a caching error.
After trying many variations, I can confirm that the first solution I've tried was the correct one, meaning that:
if (Shop::isFeatureActive()) {
$currentIdShop = Shop::getContextShopID();
Shop::setContext(Shop::CONTEXT_SHOP, $currentIdShop);
}
needs to be added ad the beginning of the "displayForm()" function for it to work when selecting a single shop. Values are now correctly saved in the database. With a little bit extra logic it can be arranged to behave differently (if needed) when saving for "All shops" context.
I want to add Department field in User entity, so I decided to extend the default UserCrudController from this package https://github.com/Laravel-Backpack/PermissionManager.
So I created a custom controller with this command php artisan make:controller Admin\UserController
In file \app\Providers\AppServiceProvider.php I also add this (as instructed)
public function register()
{
$this->app->bind(
\Backpack\PermissionManager\app\Http\Controllers\UserCrudController::class,
\App\Http\Controllers\Admin\UserController::class,
);
}
Then here is the content of \App\Http\Controllers\Admin\UserController
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Backpack\PermissionManager\app\Http\Controllers\UserCrudController;
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
class UserController extends UserCrudController
{
public function setupCreateOperation()
{
parent::setupCreateOperation();
$fields['departments'] = [
'label' => 'Departments',
'type' => 'select2_multiple',
'name' => 'tags',
'entity' => 'tags',
'attribute' => 'name',
'model' => "App\Models\Tag",
'pivot' => true,
'wrapper' => ['class' => 'form-group col-6 col-md-4'],
'options' => (function ($query) {
return $query->where('type', 'Department')->get();
}),
];
foreach($fields as $key=>$field) {
CRUD::addField($field);
}
}
public function setupUpdateOperation()
{
parent::setupUpdateOperation();
$this->setupCreateOperation();
}
}
All seems fine, I can see all default information such as: username, email, roles, permission and my custom department fields.
The problem is, when I try to modify an existing user adding some Departments, I got this error
The email has already been taken.
The password field is required.
Somehow it's treated as new user registration. What could be the problems?
Somehow, I solved it by copying setupCreateOperation to setupUpdateOperation.
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Backpack\PermissionManager\app\Http\Controllers\UserCrudController;
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
class UserController extends UserCrudController
{
...
public function setupUpdateOperation()
{
parent::setupUpdateOperation();
$fields['departments'] = [
'label' => 'Departments',
'type' => 'select2_multiple',
'name' => 'tags',
'entity' => 'tags',
'attribute' => 'name',
'model' => "App\Models\Tag",
'pivot' => true,
'wrapper' => ['class' => 'form-group col-6 col-md-4'],
'options' => (function ($query) {
return $query->where('type', 'Department')->get();
}),
];
foreach($fields as $key=>$field) {
CRUD::addField($field);
}
}
}
Hopefully there's a shorter way to reduce this duplication.
I'm using EasyAmdin 3 with Symfony 5 and I have a OneToMany relation between Challenge and Encadrement. Defined In Challenge.php:
/**
* #ORM\OneToMany(targetEntity=Encadrement::class, mappedBy="challengePartner")
*/
private $bornes;
I made a CRUDController for Challenge, and I want to be able to add Encadrement directly when creating/editing a Challenge. I did this :
AssociationField::new('bornes'),
I can choose between all the Encadrement already created. But what I want is to be able to multiple add Encadrement and I can't find how to do this. I tried making my own EncadrementType :
class EncadrementType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, array(
"label" => "Nom"
))
->add('low', IntegerType::class, array(
"label" => "Borne basse"
))
->add('high', IntegerType::class, array(
"label" => "Borne haute"
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Encadrement::class,
]);
}
}
And something like this in the ChallengeCRUD:
AssociationField::new('bornes')->setFormType(EncadrementType::class),
But I get this error when I don't even use those options:
An error has occurred resolving the options of the form "App\Form\EncadrementType": The options "class", "multiple", "query_builder" do not exist. Defined options are: "action", "allow_extra_fields", "allow_file_upload", "attr", "attr_translation_parameters", "auto_initialize", "block_name", "block_prefix", "by_reference", "compound", "constraints", "csrf_field_name", "csrf_message", "csrf_protection", "csrf_token_id", "csrf_token_manager", "data", "data_class", "disabled", "ea_crud_form", "empty_data", "error_bubbling", "error_mapping", "extra_fields_message", "getter", "help", "help_attr", "help_html", "help_translation_parameters", "inherit_data", "invalid_message", "invalid_message_parameters", "is_empty_callback", "label", "label_attr", "label_format", "label_html", "label_translation_parameters", "legacy_error_messages", "mapped", "method", "post_max_size_message", "property_path", "required", "row_attr", "setter", "translation_domain", "trim", "upload_max_size_message", "validation_groups".
I tried adding the multiple option to the AssociationField but it does nothing:
AssociationField::new('bornes')->setFormTypeOption("multiple","true"),
I'm stuck there, thanks for any help !
Try CollectionField like this:
<?php
yield CollectionField::new('bornes')
->setFormTypeOptions([
'delete_empty' => true,
'by_reference' => false,
])
->setEntryIsComplex(false)
->setCustomOptions([
'allowAdd' => true,
'allowDelete' => true,
'entryType' => EncadrementType::class,
'showEntryLabel' => false,
])
;
I have started the latest tutorial for Laminas.
The routing for a new module called Provider is not working
A 404 error occurred
Page not found.
The requested URL could not be matched by routing.
on looking at my Module.php code I see:
getConfig() is not called but
getServiceConfig() and getControllerConfig() are.
getConfig in the Application module is not called either
<?php
namespace Provider;
use Laminas\Db\Adapter\AdapterInterface;
use Laminas\Db\ResultSet\ResultSet;
use Laminas\Db\TableGateway\TableGateway;
use Laminas\ModuleManager\Feature\AutoloaderProviderInterface;
use Laminas\ModuleManager\Feature\ConfigProviderInterface;
class Module implements ConfigProviderInterface, AutoloaderProviderInterface
{
public function getConfig()
{
die ("getConfig");
return include __DIR__ . '/../config/module.config.php';
}
public function getAutoloaderConfig()
{
//die ("getAutoloaderConfig");
//return array(
// 'Laminas\Loader\StandardAutoloader' => array(
// 'namespaces' => array(
// __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
// ),
// ),
//);
}
public function getServiceConfig()
{
//die ("getServiceConfig");
return [
'factories' => [
Model\ProviderTable::class => function($container) {
$tableGateway = $container->get(Provider\ProviderTableGateway::class);
return new Model\ProviderTable($tableGateway);
},
Model\ProviderTableGateway::class => function ($container) {
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\Album());
return new TableGateway('provider', $dbAdapter, null, $resultSetPrototype);
},
],
];
}
public function getControllerConfig()
{
//die ("getControllerConfig");
return [
'factories' => [
Controller\ProviderController::class => function($container) {
return new Controller\ProviderController(
$container->get(Model\ProviderTable::class)
);
},
],
];
}
}
You need to enable development mode. run composer development-enable to active development mode.
Maybe the composer json is not updated (my-application/composer.json)
"autoload": {
"psr-4": {
"Application\\": "module/Application/src/",
"Provider\\": "module/Provider/src/"
}
},
Update autoload classmap:
composer dump-autoload
https://docs.laminas.dev/tutorials/getting-started/modules/#autoloading
Have you added router configuration?
In your attached code above you have the following function :
public function getConfig()
{
//die ("getConfig"); // BE SURE YOU REMOVE THIS LINE
return include __DIR__ . '/../config/module.config.php';
}
it's include file for additional settings. in this file "/../config/module.config.php" you should add your router configuration. It should look like this:
return [
//... other setting
'router' => [
'routes' => [
// Literal route named "home"
'home' => [
'type' => 'literal',
'options' => [
'route' => '/',
'defaults' => [
'controller' => 'Application\Controller\IndexController',
'action' => 'index',
],
],
],
// Literal route named "contact"
'contact' => [
'type' => 'literal',
'options' => [
'route' => 'contact',
'defaults' => [
'controller' => 'Application\Controller\ContactController',
'action' => 'form',
],
],
],
],
],
];
more reading can be found https://docs.laminas.dev/laminas-router/routing/#simple-example-with-two-literal-routes
As mentioned before any time you add a custom module you will need to add an entry for the autoloader in composer.json and run the dump-autoload. You will also need to add an entry in the root level /config/modules.config.php file. Is there currently an entry for Application? If memory serves and your working from the examples the last two should be Application, then Album. Verify those are there and that the application is in development mode. You can check the current mode with "composer development-status". Just check composer.json in the top level and look for the "scripts" entry. The key is the command to pass to composer.
Also, be mindful of using the interfaces when configuring the application in the Module class. The Module feature methods are reserved for closures as they will not be cached when you disable development mode. Instead use the corresponding service manager array keys. that can be found here:
Service manager config:
https://docs.laminas.dev/laminas-servicemanager/configuring-the-service-manager/
Corresponding module manager feature config:
https://docs.laminas.dev/laminas-modulemanager/module-manager/
I suppose its worth mentioning that most if not all of the Feature interface methods map directly to a default pluginmanager implementation, ergo a specialized service manager.
my first question to this site is a little difficult to describe.
I am quite new to Symfony, startet with 3.2 and updated recently to 3.3.5 (not sure if relevant for the problem).
I tried to use CraueFormFlowBundle (multistep form bundle) but cannot get it to work.
The problem is that trying to access the flow results in an exception:
Error: Call to a member function getCurrentRequest() on null
Symfony\Component\Debug\Exception\ FatalErrorException
in vendor/craue/formflow-bundle/Form/FormFlow.php (line 191)
Line 191 shows: $currentRequest = $this->requestStack->getCurrentRequest();
Modifying the FormFlow.php with dump line shows that $this->requestStack is null.
I have not enough knowledge about this bundle to know where to start looking for the problem.
The flow definition is based on the location example:
namespace EngineeringBundle\Form;
use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Form\FormFlowInterface;
class SelectExaminationFlow extends FormFlow
{
/**
* {#inheritDoc}
*/
protected function loadStepsConfig()
{
dump("loadStepsConfig");
return array(
array(
'label' => 'engineering.discipline',
'form_type' => new SelectExaminationStep1Form(),
),
array(
'label' => 'engineering.date',
'form_type' => new SelectExaminationStep2Form(),
'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) {
return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveRegion();
},
),
array(
'label' => 'confirmation',
),
);
}
The form definition is also quite simple and works without problems:
class SelectExaminationStep1Form extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump("buildForm");
$builder
->add('id', HiddenType::class)
->add('discipline', EntityType::class, array(
'class' => 'EngineeringBundle:Discipline',
'choice_label' => 'disciplineName',
'label' => 'engineering.discipline.label'
)
);
}
public function getName() {
return $this->getBlockPrefix();
}
public function getBlockPrefix() {
return 'createEngineeringStep1';
}
}
services.yml:
EngineeringBundle\Form\SelectExaminationFlow:
parent: craue.form.flow
autowire: false
autoconfigure: false
public: true
engineering.form_flow:
alias: EngineeringBundle\Form\SelectExaminationFlow
public: true
Controller:
/**
* #Route("create", name="engineering_create")
*/
public function createAction()
{
return $this->processFlow(new ExaminationDate(), $this->get('engineering.form_flow'));
}
Thanks in advance
Sebastian
I was having the same problem, resolved it by adding a constructor to vendor/craue/formflow-bundle/Form/FormFlow.php with the following content:
public function __construct(RequestStack $requestStack, FormFactoryInterface $formFactory, DataManagerInterface $dataManager, EventDispatcherInterface $eventDispatcher) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
$this->dataManager = $dataManager;
$this->eventDispatcher = $eventDispatcher;
}
Make sure to place it after all setter-methods. Problem seems to be related to a symfony update.