Override Class in PHPUnit Feature Test - laravel

I am trying to test a custom Artisan command which does multiple things and then at the end does a csv import. I instantiate the object manually new CsvDirectDatabaseImporter inside the artisan command. This runs a method called import() which imports from csv to database using LOAD DATA LOCAL INFILE which is not supported by SQLite. Since I want my tests to run in memory I want to override (or mock/stub not sure what the correct term is) the import method on the CsvDirectDatabaseImporter class so it doesn't do anything during the import call. This way the rest of my tests will work (I know now I'm not testing the actual import) How would I go around this:
Here is a simplified version my Artisan Class:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use App\Services\CsvDirectDatabaseImporter\CsvDirectDatabaseImporter;
use App\Services\CsvDirectDatabaseImporter\MyColumns;
class DataMartImport extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'myimport:import
{year : The year of processing} ';
/**
* The console command description.
*
* #var string
*/
protected $description = 'My Import';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$year = $this->argument('year');
// Copy the file to processing location.
File::copy($files[0], $processing_file);
// Import the CSV File.
$csvImporter = new CsvDirectDatabaseImporter($processing_file, 'myTable', new MyColumns());
$csvImporter->import();
}
}
A simplified version of a Feature test of running my custom artisan command:
<?php
namespace Tests\Feature\Console\DataMart;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
use Illuminate\Support\Facades\Config;
use Mockery as m;
use App\Services\CsvDirectDatabaseImporter\DataMartColumns;
use App\Services\CsvDirectDatabaseImporter\CsvDirectDatabaseImporter;
use Illuminate\Support\Facades\Artisan;
class MyImportTest extends TestCase
{
public function testImportFoldersGetCreatedIfNoDirectory()
{
$year = 2019;
$this->artisan('myimport:import', ['year' => $year]);
// Assertions of items go here unrelated to the actual database import.
}
}
CSVImorter Class
<?php
namespace App\Services\CsvDirectDatabaseImporter;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Symfony\Component\HttpFoundation\File\File as CSV_File;
class CsvDirectDatabaseImporter {
/**
* File to import.
*
* #var \Symfony\Component\HttpFoundation\File\File
*/
private $file;
/**
* Table name.
*
* #var string
*/
private $table;
/**
* Fields terminated by.
*
* #var string
*/
public $fieldsTerminatedBy = '\',\'';
/**
* Enclosed by.
*
* #var string
*/
public $enclosedBy = '\'"\'';
/**
* Lines terminated by.
*
* #var string
*/
public $linesTerminatedBy = '\'\n\'';
/**
* Ignore first row.
*
* #var bool
*/
public $ignoreFirstRow = true;
/**
* Csv Import columns.
*
* #var array
*/
public $columns;
/**
* CsvImporter constructor.
*
* #param string $path
* The full temporary path to the file
*/
public function __construct(string $path, $table, CsvDirectDatabaseImportColumns $columns)
{
$this->file = new CSV_File($path);
$this->table = $table;
$this->columns = $columns->getColumns();
}
/**
* Import method used for saving file and importing it using database query.
*/
public function import()
{
// Normalize line endings
$normalized_file = $this->normalize($this->file);
// Import contents of the file into database
return $this->importFileContents($normalized_file, $this->table, $this->columns);
}
/**
* Convert file line endings to uniform "\r\n" to solve for EOL issues
* Files that are created on different platforms use different EOL characters
* This method will convert all line endings to Unix uniform
*
* #param string $file_path
* #return string $file_path
*/
protected function normalize($file_path)
{
// Load the file into a string.
$string = #file_get_contents($file_path);
if (!$string) {
return $file_path;
}
// Convert all line-endings using regular expression.
$string = preg_replace('~\r\n?~', "\n", $string);
file_put_contents($file_path, $string);
return $file_path;
}
/**
* Import CSV file into Database using LOAD DATA LOCAL INFILE function
*
* NOTE: PDO settings must have attribute PDO::MYSQL_ATTR_LOCAL_INFILE => true
*
* #param string $file_path
* File path.
* #param string $table_name
* Table name.
* #param array $columns
* Array of columns.
*
* #return mixed Will return number of lines imported by the query
*/
private function importFileContents($file_path, $table_name, $columns)
{
$prefix = config('database.connections.mysql.prefix');
$query = '
LOAD DATA LOCAL INFILE \'' . $file_path . '\' INTO TABLE `' . $prefix . $table_name . '`
FIELDS TERMINATED BY ' . $this->fieldsTerminatedBy . '
ENCLOSED BY ' . $this->enclosedBy . '
LINES TERMINATED BY ' . $this->linesTerminatedBy . '
';
if ($this->ignoreFirstRow) {
$query .= ' IGNORE 1 ROWS ';
}
if ($columns) {
$query .= '(' . implode(",\n", array_keys($columns)) . ')';
$query .= "\nSET \n";
$sets = [];
foreach ($columns as $column) {
$sets[] = $column['name'] . ' = ' . $column['set'];
}
$query .= implode(",\n", $sets);
}
return DB::connection()->getPdo()->exec($query);
}
}
CsvDirectDatabaseImportColumns Interface
<?php
namespace App\Services\CsvDirectDatabaseImporter;
interface CsvDirectDatabaseImportColumns
{
/**
* Returns array of columns.
*
* Ex:
* '#user_id' => [
* 'name' => 'user_id',
* 'set' => '#user_id',
* ],
* '#agent_number' => [
* 'name' => 'agent_number',
* 'set' => 'LEFT(#agent_number, 7)',
* ],
*
* The key should be the column name of the csv but add # in front. The name
* will be the database table. The set will be what it s se
*
* #return array
*/
public function columns();
/**
* Returns columns.
*
* #return array
* Columns.
*/
public function getColumns();
}
Things I tried
$mock = $this->createMock(CsvDirectDatabaseImporter::class);
$mock->method('import')->willReturn(true);
$this->app->instance(CsvDirectDatabaseImporter::class, $mock);
$this->artisan('datamart:import', ['year' => $year]);
But no luck there. It still runs the regular import() method.

So I have tried to reproduce what I think you need into a simple example
Let's say we have this command
<?php
namespace App\Console\Commands;
use Exception;
use Illuminate\Console\Command;
class Foo extends Command
{
protected $signature = 'foo';
public function __construct()
{
parent::__construct();
}
public function handle()
{
if ($this->import()) {
$this->info('Success');
} else {
$this->error('Failed');
}
}
public function import()
{
throw new Exception('An exception that should not be thrown');
}
}
The import method throws an exception but here's how to mock it to return true
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Console\Commands\Foo;
class FooCommandTest extends TestCase
{
public function testExample()
{
$mock = $this->getMockBuilder(Foo::class)->setMethods(['import'])->getMock();
$mock->method('import')->willReturn(true);
$this->app->instance('App\Console\Commands\Foo', $mock);
$this->artisan('foo')
->expectsOutput('Success')
->assertExitCode(0);
}
}
This test passes with two successful assertion, so you can adjust your command code to use a dedicated method for the import
Hope this helps

Related

Using markdown in Mailable: Passing an $environment into the "CommonMarkConverter" constructor is deprecated

What I want to archive: I'd like to create a Zammad ticket using the Zammad-api but also parse markdown.
To do so, I created a custom channel to send a notification to the Zammad Helpdesk system using Zammad's API.
This is the specific class:
<?php
namespace App\Channels;
use Illuminate\Mail\Mailable;
class ZammadMessage extends Mailable
{
/**
* The issuer of the ticket.
*
* #var string
*/
public $from;
/**
* The text content of the message.
*
* #var string
*/
private $content;
public function __construct($from, $content = '')
{
$this->from = $from;
$this->content = $content;
}
public static function create($from = '', $content = '')
{
return new static($from, $content);
}
/**
* Set the text content of the message.
*
* #param $content
*
* #return $this
*/
public function content($content)
{
$this->content = $content;
return $this;
}
public function asMarkdown()
{
$this->build();
$this->body = $this->buildView();
return $this;
}
public function build()
{
return $this->from($this->from)
->markdown('emails.contact.submitted', ['data' => $this->content]);
}
/**
* Set the issuer of the ticket.
*
* #param $address
* #param string $name
*
* #return $this
*/
public function from($address, $name = 'null'): static
{
$this->from = $address;
return $this;
}
}
Using this class by my notification class
public function toTicket($notifiable)
{
$address = $notifiable instanceof AnonymousNotifiable
? collect($notifiable->routeNotificationFor('zammad'))->first()
: $notifiable->email;
return ZammadMessage::create()
->from($address)
->content($this->content)
->asMarkdown();
}
I am getting this error:
PHP Deprecated: Passing an $environment into the "League/CommonMark/CommonMarkConverter" constructor is deprecated in 1.6 and will not be supported in 2.0; use MarkdownConverter instead. See https://commonmark.thephpleague.com/2.0/upgrading/consumers/#commonmarkconverter-and-githubflavoredmarkdownconverter-constructors for more details. in /var/www/html/vendor/league/commonmark/src/CommonMarkConverter.php on line 43
That E_USER_DEPRECATED error is not stopping code execution so you should be able to ignore it. Double-check your error_reporting setting in php.ini and/or any similar settings in your framework and adjust as needed.

Query\Builder::keyBy does not exist (Laravel)

I've created a command and I am trying to query my database and group my results by a key but I keep recieving this error:
In Builder.php line 2512:
Method Illuminate\Database\Query\Builder::keyBy does not exist.
Laravel version is 5.6.4
Command code:
<?php
namespace App\Console\Commands;
use App\User;
use Illuminate\Console\Command;
class TwitchPointScanner extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'command:twitchPointScanner';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Twitch Point Scanner';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$usersDistributors = User::where('point_distributor', 1)
->inRandomOrder()
->get();
$usersTwitchVerified = User::where('point_distributor', 0)
->whereNotNull('twitch_username')
->keyBy('twitch_username')
->get();
$this->line('Distributors ---');
foreach($usersDistributors as $user) {
$this->line($user->id . ': '.$user->twitch_username);
}
$this->line('Point gainers ---');
foreach($usersTwitchVerified as $user) {
$this->line($user->id . ': '.$user->twitch_username);
}
}
}
keyBy is a collection method, so you need to get the data first:
$usersTwitchVerified = User::where('point_distributor', 0)
->whereNotNull('twitch_username')
->get()
->keyBy('twitch_username');

Laravel 5.4 - Passing Data from Controller to Artisan Handle

To get things started, I made a custom Artisan Command called MySqlRestore.
It simply restores the database using the dumped sql file.
Here's my MySqlRestore code:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class MySqlRestore extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'db:restore';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Restores database using info from .env';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$sqlfile = //data from controller;
$ds = DIRECTORY_SEPARATOR;
$host = env('DB_HOST');
$username = env('DB_USERNAME');
$password = env('DB_PASSWORD');
$database = env('DB_DATABASE');
$mysqlpath = 'C:\xampp\mysql\bin\mysql';
$path = 'C:\salesandinventory\Backups\\';
$command = sprintf($mysqlpath . ' --user=' . $username . ' --password=' . $password . ' --host=' . $host . ' ' . $database . ' < ' . $path . $sqlfile);
exec($command);
}
}
Now on this line $sqlfile = //data from controller;, I need the data from my controller.
Here's how my controller looks like:
<?php
namespace App\Http\Controllers;
use App\Database;
use Illuminate\Http\Request;
use Artisan;
class DatabaseController extends Controller
{
public function index()
{
return view('database.index');
}
public function restoreDatabase(Request $request)
{
$sqlfile = $request->sqlfile; // the data I need.
Artisan::call('db:restore');
return view('database.index');
}
}
Now I don't have any idea how to pass $sqlfile = $request->sqlfile; this data from my controller into my Artisan handle function.
Data's are passed through the protected $signature using curly braces {dataName}
e.g
protected $signature = 'db:restore {dataName}'
and it is called using
$this->argument('dataName');
In your controller
Artisan::call('db:restore',['test'=> $test]);
namespace App\Console\Commands;
use Illuminate\Console\Command;
class MySqlRestore extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'db:restore {test}';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Restores database using info from .env';
public $sqlFile;
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$sqlfile = $this->argument('test');
$ds = DIRECTORY_SEPARATOR;
$host = env('DB_HOST');
$username = env('DB_USERNAME');
$password = env('DB_PASSWORD');
$database = env('DB_DATABASE');
$mysqlpath = 'C:\xampp\mysql\bin\mysql';
$path = 'C:\salesandinventory\Backups\\';
$command = sprintf($mysqlpath . ' --user=' . $username . ' --password=' . $password . ' --host=' . $host . ' ' . $database . ' < ' . $path . $sqlfile);
exec($command);
}
}
Call it like this
Artisan::call('db:restore',['test'=> $test]);
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class MySqlRestore extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'db:restore';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Restores database using info from .env';
public $sqlFile;
/**
* Create a new command instance.
*
* #return void
*/
public function __construct($sqlFile)
{
$this->sqlFile = $sqlFile;
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$sqlfile = $this->sqlFile;
$ds = DIRECTORY_SEPARATOR;
$host = env('DB_HOST');
$username = env('DB_USERNAME');
$password = env('DB_PASSWORD');
$database = env('DB_DATABASE');
$mysqlpath = 'C:\xampp\mysql\bin\mysql';
$path = 'C:\salesandinventory\Backups\\';
$command = sprintf($mysqlpath . ' --user=' . $username . ' --password=' . $password . ' --host=' . $host . ' ' . $database . ' < ' . $path . $sqlfile);
exec($command);
}
}
Controller
<?php
namespace App\Http\Controllers;
use App\Database;
use Illuminate\Http\Request;
use Artisan;
class DatabaseController extends Controller
{
public function index()
{
return view('database.index');
}
public function restoreDatabase(Request $request)
{
$sqlfile = $request->sqlfile; // the data I need.
Artisan::call('db:restore',['sqlFile'=>$sqlFile]);
return view('database.index');
}
}
Your controller should not be executing Artisan commands, especially potentially long-running ones like executing a database back-up.
Instead, consider dispatching a queued job that performs the restore. You can then return control to the user and they can get on with things they need to do, instead of keeping a web page open that could possibly timeout and leave the database in a corrupt state.
class DatabaseRestoreController extends Controller
{
public function store(Request $request)
{
dispatch(new RestoreDatabaseJob($request->input('filename')));
return redirect()->back()->withSuccess('Restoring database.');
}
}
And the job class itself:
class RestoreDatabaseJob implements ShouldQueue
{
use InteractsWithQueue;
public $filename;
public function __construct($filename)
{
$this->filename = $filename;
}
public function handle()
{
Artisan::call('db:restore', [
'filename' => $this->filename,
]);
// You can notify user restore completed
// Send email, SMS via notification etc.
}
}

What is the best way for reusable values throughout the application in Symfony 3?

I want to have a file or list that I can update easily with values that might change throughout my application.
I don't really want to hard code text values into the templates. I prefer to have all of these values in one place and labelled correctly.
Examples of values that might get updated are:
Page title
Logo text
Brand or company name
I have thought about two options:
Add them to the twig config in config.yml. This is a bit messy and doesn't seem organised if I decide to put a lot of values there.
Make a database table for these and include the entity in each controller where I need to use the values. This might be creating too much work.
Are there any other options or are one of these more suitable?
Thank you.
You need to create a twig function and use it to return the value you want. For example:
namespace AppBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
class TwigExtension extends \Twig_Extension implements ContainerAwareInterface
{
use ContainerAwareTrait;
/**
* #var ContainerInterface
*/
protected $container;
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('parameter', function($name)
{
try {
return $this->container->getParameter($name);
} catch(\Exception $exception) {
return "";
}
})
);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'app.twig.extension';
}
}
This will create a function called parameter and once you call it in twig {{ parameter('my.parameter') }} it will return the parameter. You need to load it as a service, which you can do by adding the following to your services.yml file:
app.twig.extension:
class: AppBundle\Twig\TwigExtension
calls:
- [setContainer, ["#service_container"]]
tags:
- { name: twig.extension }
From personal experience people usually want to be able to change some of the parameters. This is why I usually prefer to create a Setting or Parameter entity which would look something like this:
/**
* Setting
*
* #ORM\Table(name="my_parameters")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ParameterRepository")
*/
class Parameter
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="parameter_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="value", type="text", nullable=true)
*/
private $value;
/**
* #param string|null $name
* #param string|null $value
*/
public function __construct($name = null, $value = null)
{
$this->setName($name);
$this->setValue($value);
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Parameter
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set value
*
* #param string $value
*
* #return Parameter
*/
public function setValue($value = null)
{
$this->value = serialize($value);
return $this;
}
/**
* Get value
*
* #return string
*/
public function getValue()
{
$data = #unserialize($this->value);
return $this->value === 'b:0;' || $data !== false ? $this->value = $data : null;
}
}
Then I would add a CompilerPass which will help get all of the parameters from the database and cache them so that your app doesn't make unnecessary sql queries to the database. That might look something similar to the following class:
// AppBundle/DependencyInjection/Compiler/ParamsCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParamsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$em = $container->get('doctrine.orm.default_entity_manager');
$settings = $em->getRepository('AppBundle:Parameter')->findAll();
foreach($settings as $setting) {
// I like to prefix the parameters with "app."
// to avoid any collision with existing parameters.
$container->setParameter('app.'.strtolower($setting->getName()), $setting->getValue());
}
}
}
And finally, in your bundle class (i.e. src/AppBundle/AppBundle.php) you add the compiler pass:
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\ParamsCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $builder)
{
parent::build($builder);
$builder->addCompilerPass(new ParamsCompilerPass(), , PassConfig::TYPE_AFTER_REMOVING);
}
}
Now you can create a DoctrineFixture template to load the parameters you use all the time. With the TwigExtension you will still be able to call the parameter from the twig template and you can create a web UI to change some of the parameters/settings.

Magento 2 - Set product attribute to use default values

I want to check Use as Default for all product for a particular store view
I use this code
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$collection = $this->_productCollectionFactory->create();
foreach($collection as $data){
$product=$this->_product->setStoreId(1)->load($data->getEntityId());
$product->setData('name',false);
$product->save();
}
But this is removing product from categories.
Can you please let me know how I can check Use as default checkbox programmatically.
Magento 2.1.3 EE
The following solution creates a CLI command to directly manipulate the database and delete store specific product attribute information. It was written for Magento Enterprise edition, so if you're using Community Edition you'll have to modify this code to utilize entity_id instead of row_id.
Please be careful with this. The solution laid forth here bypasses model classes and performs direct delete queries on the default database connection's catalog_product_entity_datetime, catalog_product_entity_decimal, catalog_product_entity_int, catalog_product_entity_text, and catalog_product_entity_varchar tables. Back up your database first.
Step 1: Create the module
app/code/StackOverflow/Question40177336/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'StackOverflow_Question40177336',
__DIR__
);
app/code/StackOverflow/Question40177336/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="StackOverflow_Question40177336" setup_version="0.0.1"/>
</config>
app/code/StackOverflow/Question40177336/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\Console\CommandListInterface">
<arguments>
<argument name="commands" xsi:type="array">
<item name="stackoverflow_question40177336" xsi:type="object">StackOverflow\Question40177336\Console\Command\Product\UseDefaultValue</item>
</argument>
</arguments>
</type>
</config>
app/code/StackOverflow/Question40177336/Console/Command/Product/UseDefaultValue.php
<?php
namespace StackOverflow\Question40177336\Console\Command\Product;
use Magento\Catalog\Model\Product;
use Magento\Eav\Setup\EavSetup;
use Magento\Framework\App\ResourceConnection;
use Magento\Store\Model\StoreManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UseDefaultValue extends Command
{
/**
* flag indicating if the command has been initialized yet
*
* #var bool
*/
protected $initialized = false;
/**
* The attribute_id to use for the current command.
*
* #var int
*/
protected $attributeId;
/**
* The row_id values(s) to use for the command (if any).
*
* #var array|bool
*/
protected $rowIds;
/**
* The store_id to use for the current command.
*
* #var int
*/
protected $storeId;
/**
* The table name to use for the current command.
*
* #var string
*/
protected $tableName;
/**
* #var \Magento\Framework\DB\Adapter\AdapterInterface
*/
protected $connection;
/**
* #var EavSetup
*/
protected $eavSetup;
/**
* #var StoreManagerInterface
*/
protected $storeManager;
public function __construct(
EavSetup $eavSetup,
ResourceConnection $resourceConnection,
StoreManagerInterface $storeManager
) {
$this->connection = $resourceConnection->getConnection();
$this->eavSetup = $eavSetup;
$this->storeManager = $storeManager;
parent::__construct();
}
/**
* Configures the current command.
*/
protected function configure()
{
$this
->setName('catalog:product:attributes:use-default-value')
->setDescription('Removes store specific data from a product(s) of given attribute code.')
->addArgument(
'attribute_code',
InputArgument::REQUIRED,
'Attribute Code'
)
->addArgument(
'store',
InputArgument::REQUIRED,
'Store code or store_id (cannot be \'admin\' or \'0\')'
)
->addArgument(
'sku',
InputArgument::OPTIONAL,
'Sku (omit to apply to all products)'
)
;
}
/**
* Executes the current command.
*
* #param InputInterface $input An InputInterface instance
* #param OutputInterface $output An OutputInterface instance
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->init($input);
$conn = $this->connection;
$bind = [
$conn->quoteInto('store_id = ?', $this->getStoreId()),
$conn->quoteInto('attribute_id = ?', $this->getAttributeId())
];
if ($this->getRowIds()) {
$bind[] = $conn->quoteInto('row_id IN (?)', $this->getRowIds());
}
$rows = $conn->delete($this->getTableName(), $bind);
$output->writeln($rows.' rows deleted.');
}
/**
* Return the row_id value(s) to use for the command (if any).
*
* #return array|boolean
*/
protected function getRowIds()
{
if (!$this->initialized) {
$this->errorInit(__METHOD__);
}
return $this->rowIds;
}
/**
* Initializes some class properties.
*
* #param InputInterface $input
*/
protected function init(InputInterface $input)
{
if (!$this->initialized) {
$attributeCode = trim($input->getArgument('attribute_code'));
if ($attributeCode == '') {
throw new \RuntimeException(__('attribute_code is required.'));
} elseif (is_numeric($attributeCode)) {
throw new \RuntimeException(__('attribute_code cannot be numeric.'));
}
$attribute = $this->eavSetup->getAttribute(
Product::ENTITY,
$attributeCode
);
if (!$attribute) {
throw new \RuntimeException(__('Invalid attribute_code "%1"', $attributeCode));
}
$backendType = $attribute['backend_type'];
$allowedTypes = ['datetime','decimal','int','text','varchar'];
if (!in_array($backendType, $allowedTypes)) {
throw new \RuntimeException(__(
'backend_type "%1" is not allowed. Allowed types include: %2',
$backendType,
implode(', ', $allowedTypes)
));
}
$this->tableName = $this->connection->getTableName('catalog_product_entity_'.$backendType);
$this->attributeId = (int) $attribute['attribute_id'];
$store = $this->storeManager->getStore($input->getArgument('store'));
if ($store->getCode() == 'admin') {
throw new \RuntimeException(__('Admin Store is not allowed for this command.'));
}
$this->storeId = (int) $store->getId();
$sku = trim($input->getArgument('sku'));
if ($sku != '') {
$sql = $this->connection->select()
->from($this->connection->getTableName('catalog_product_entity'), 'row_id')
->where('sku = ?', $sku)
;
$rowIds = $this->connection->fetchCol($sql);
if (!$rowIds) {
throw new \RuntimeException(__('Invalid Sku "%1"', $sku));
}
foreach ($rowIds as $k => $v) {
$rowIds[$k] = (int) $v;
}
$this->rowIds = $rowIds;
} else {
$this->rowIds = false;
}
$this->initialized = true;
}
}
/**
* Returns the attribute_id to use for the current command.
*
* #return int
*/
protected function getAttributeId()
{
if (!$this->attributeId) {
$this->errorInit(__METHOD__);
}
return $this->attributeId;
}
/**
* Return the store id to use for the current command.
*
* #param InputInterface $input
*/
protected function getStoreId()
{
if (!$this->storeId) {
$this->errorInit(__METHOD__);
}
return $this->storeId;
}
/**
* Return the qualified table name to use for the current command.
*
* #param InputInterface $input
*/
protected function getTableName()
{
if (!$this->tableName) {
$this->errorInit(__METHOD__);
}
return $this->tableName;
}
/**
* Throws an exception.
*
* #param string $methodName
* #throws \LogicException
*/
protected function errorInit($methodName)
{
throw new \LogicException(
__('Command has not been intialized. Call UseDefaultValue::init() before calling '.$methodName));
;
}
}
Step 2: Enable the module
php -f bin/magento module:enable StackOverflow_Question40177336
Step 3: Utilize your new CLI command.
The command has two required arguments, attribute_code and store. Store can be either the ID or Code. Admin store is not allowed for obvious reasons. The command also has an optional third parameter of SKU if you wish to only target a specific SKU (omitting this applies to all products).
For example, if you wanted to delete all "name" values from the "default" store view your command would be as follows:
php -f bin/magento catalog:product:attributes:use-default-value name default

Resources