Magento – Inchoo http://inchoo.net Magento Design and Magento Development Professionals - Inchoo Wed, 22 Nov 2017 14:26:57 +0000 en-US hourly 1 https://wordpress.org/?v=4.8.1 Why you should go to MageTestFest if you are a Magento developer http://inchoo.net/life-at-inchoo/go-magetestfest-magento-developer/ http://inchoo.net/life-at-inchoo/go-magetestfest-magento-developer/#respond Fri, 03 Nov 2017 13:50:31 +0000 http://inchoo.net/?p=31014 When a conference is advertised as “Magento. Software Testing. Party.” and Yireo stands behinds its organization, there is really not much left to say in order to convince you to attend MageTestFest that’s happening November 15th – 18th 2017! There are no doubts – you must clear your schedule and head off to this international...

The post Why you should go to MageTestFest if you are a Magento developer appeared first on Inchoo.

]]>
When a conference is advertised as “Magento. Software Testing. Party.” and Yireo stands behinds its organization, there is really not much left to say in order to convince you to attend MageTestFest that’s happening November 15th – 18th 2017!

There are no doubts – you must clear your schedule and head off to this international developer oriented event that has one single focus: TESTING! Vital for clean coding, testing is a topic that can always break the unbearable silence among developers that are just meeting each other.

But let’s be serious for a moment here. Testing should be a fundamental part of your work if you are a developer. Guys behind MageTestFest saw the need for full-depth conference about testing, where everybody is talking about the same topic, which results in extreme focus, learning and fun. And also for future cleaner code, fewer screw-ups and happier customers.

Who are the speakers?

Only proven experts on the subject! Sebastian Bergmann, Mathias Verraes, Vinai Koop, Fabian Schmengler, James Cowie, Tom Erskine, Igor Minailo and Jisse Reitsma will blow you away with knowledge they are willing to share in order for you to become an even better developer!

 

Four exciting days are ahead of us all – 1 conference day, 2 workshops (PHPUNIT, DDD) and 1 contribution day/hackathon.

Meet the Inchooers, they’ll have the goodies!

Inchoo troops will also be there! Look for our green banner where Stjepan Udovicic and Luka Rajcevic will meet you with open hands (that will be filled with Inchoo swag)! T-shirts, notebooks, stickers, be sure to grab yours while you can! 😉


Go to MageTestFest, invest in a future with fewer screw-ups!

The post Why you should go to MageTestFest if you are a Magento developer appeared first on Inchoo.

]]>
http://inchoo.net/life-at-inchoo/go-magetestfest-magento-developer/feed/ 0
Keeping an eye on small things in eCommerce projects http://inchoo.net/ecommerce/keeping-eye-small-things-ecommerce-projects/ http://inchoo.net/ecommerce/keeping-eye-small-things-ecommerce-projects/#comments Tue, 31 Oct 2017 12:23:02 +0000 http://inchoo.net/?p=30978 When managing projects, one usually focuses on big things: biggest costs, biggest features, biggest risks, etc. The same is with building an eCommerce site – the biggest, most important things are, well, most important. But large topics are not the be-all and end-all of the project. In this post, we will illustrate how tiny issues...

The post Keeping an eye on small things in eCommerce projects appeared first on Inchoo.

]]>
When managing projects, one usually focuses on big things: biggest costs, biggest features, biggest risks, etc. The same is with building an eCommerce site – the biggest, most important things are, well, most important.

But large topics are not the be-all and end-all of the project.

In this post, we will illustrate how tiny issues can have an outsized influence on the project. Through analysis of several examples from our experience, we will try to understand how small perturbations shape the course of the project.

So let’s start with something totally relevant, let’s start with – Napoleon.

We all know the story. He conquered Europe, and then turned his sights on Russia. His conquest started with an army of 680 000 men, and in a short time marched into Moscow. Russians evacuate Moscow and burn down three quarters of it! Napoleon, short on supplies retreats from the empty city. During the retreat, he loses most of his army. Out of 600 000 men, only 30 000 survive, and only 1 000 are fit for service.

A great story of brilliant defensive strategy.

Or is it?

In 2001, workers digging trenches for telephone cables in Vilnius, Lithuania found over 2 000 skeletons, stacked three on top of each other, arranged in v-shape. Analysis revealed that those were soldiers from Napoleon’s time, and with their remains, the complete story of Napoleon’s defeat was revealed:

Napoleon starts his attack on Russia in June 1812 from Germany. Things turn downhill in Poland. The summer is unusually hot and 20 thousand horses die of thirst. This stretches and strains the supply lines and the resources are scarce. The hygiene is bad and lice are becoming a huge problem – they are visibly crawling on men and the whole army is infested. Lice carry typhus, and in a month 80 000 soldiers die or are incapacitated because of the disease.

In August Napoleon conquers Smolensk, but another 105 00 men are lost to typhus. In the next two weeks, typhus claims another 60 000 men. In September, a week after the battle of Borodino, Napoleon enters Moscow with 90 000 men. The city is deserted and burned. Napoleon requests enforcements, but out of 15 000 men sent to him, 10 000 die from typhus. The winter is coming, and supplies have run out. Napoleon decides to retreat.

In December they come to Vilnius, with only 20 000 men fit for service. Fearing the coup, Napoleon urgently leaves for Paris, and general Murat organizes the retreat.

And now it is clear: a military genius of Kutuzov, and the coldest Russian winter and all the Russian cannons heard pounding in overture to Tchaikovsky’s 1812 weren’t enough to defeat the great general. It took the smallest creature of them all, unassuming, ordinary lice to bring destruction to Grande Armée.

With that in mind, what are the small, seemingly insignificant things that have an outsized influence on our projects?

Let’s walk through few examples of the small challenges or hiccups that proved to be big in the end (whether we acted proactively to resolve them and prevent the excrement hitting the fan, or we learned the hard way from addressing them too late in the game).

We will start with technical topics, and move slowly into organizational and pure project management issues.

Comment? What comment?
– When comments in code are bad

You know how developers often comment out pieces of code while working, in order to speed up a process or go around specific bugs in Magento (what bugs in Magento, you ask?!?!)

Well, it happened probably too many times that a piece of code that was commented out during development made it through to production that way, causing havoc on live site. Why is this commented out? What does it do? What did it use to do?

Lesson learned: Establish proper git branching model – And stick to it for dear life!

Leave commented out code out of committed code. Instead, keep it in git-diff – this may be subject to interpretation and depends on the project at hand, but this is a good rule of thumb. For controlling different versions of our code, we should use – version control software.

Attack of the Robots
– Ignore crawlers at your own peril!

Magento site works the same with or without the robots.txt file. The file itself does not affect the functioning of the store. The interesting thing about it is that it is one of few items that must be different between staging and live site (on staging we want to exclude everything from crawling, and on live site we are targeting specific files and folders – though usually it is a large number of them).

What happens is that after deployment, because of gitignore developer inadvertently deletes robots.txt file without noticing. Actually, nobody notices – the site works ok and everybody is celebrating new feature release.

Nobody notices, until we got a call from the client that the site is down. Next call is with the hosting provider – they will yell how they are bombarded with requests and how Google crashes our site. All because of one simple small txt file that does not affect our code or the user.

Lesson learned: Establish post-deploy check procedure – And execute it every time!

There are a lot of small tasks that need to be done and are easily forgotten during deploy. Having a checklist that we can rely on releases our mental energy to focus on executing the task at hand.

I-Track, U-Track, DDoS-Track
– Newsletters gone wild!

Newsletters are a great way to inform customers about new promotions and discounts. One large client had a big subscriber database and was sending a newsletter to them without problem for years. The URL in the newsletter was a hefty complex query, but as Magento has full-page caching, it was not a problem for the server.

At one point marketing came to a very reasonable idea to track which subscribers open the newsletter, so they could analyze and segment the campaigns. The tracking was implemented by adding a unique ID parameter to each newsletter URL, so when the user opens the newsletter, the hit is registered.

What that meant is that each user was served a unique page, Magento’s full-page cache was no longer used, and the query was executed for each hit. With a huge amount of subscribers opening their newsletter at the same time and thus executing the enormous query, the site went down. The client DDoS-ed themselves.

Lesson learned: Establish a Change Request Procedure – Make it simple, clear and stick to it!

Change is a fact for any project, that is why we have to be prepared for change. By establishing a Change Request Procedure we will make sure that relevant persons vet the change. For example, should the request be checked by Development, SEO specialist, Marketing? In our example, the solution was to setup caching to ignore the parameter – something a developer would notice in a second.

Extending the non-extendable
– Assumptions are just that!

We have a relatively large client (around 80 000 products) that wanted to track stock for a subset of products, and if a customer tries to order out of stock product, offer them a subscription to notification when the product is in stock again.

The notification is easy to do if the stock management is global, if Magento manages the stock for all products. If the stock is tracked only on a subset of products, as was the case with that client, then Magento will trigger the subscription message for all non-managed products (since their stock is 0)

We planned to go with Magento 1, so the solution was simple – extend the core to additionally check does the product have managed stock and if not, suppress the message. 30 minutes work.

Then Magento 2 became a thing. And the plan was changed, we’ll go with Magento 2. Because why not. Now, Magento 2 does not allow extending core. In general, you don’t extend a component, you write your own. In this case, we were deep in the core, we had to rework the whole Product page, which landed us with 30h of work. Add few similar customizations, and you have a project deep in red.

Lesson learned: Check your assumptions – Then double check them!

Every project is built on a series of assumptions. If one of them changes or proves wrong (we’re using Magento 2 instead of Magento 1), then the project is in danger (in this case, our estimates are way off). But if we know the assumptions we’re building our project on (dare I say: if we have them written down), then we can always check them and react when they’re changing.
Most important lesson: build a relationship of trust with the client, so you can work together when problems like these arise.

An admin, an admin! My kingdom for an admin!
– Who handles system administration?

We had a client where we took care of everything on Magento side, and everyone thought (ok, we thought) that their hosting company takes care of permissions and overall server-side setup.

However, when we came close to deploying to the live site (there was a specific environment in place where we were allowed to push changes to live in a very specific way), we brought the site down only to realize that our latest deployments failed to go to production due to server-side setup mixup. We were not in a position to fix this ourselves, hosting company wasn’t aware we expected them to handle this, the client didn’t know whose job this would have to be in the first place. A small omission in communication caused a huge issue when the push came to shove.

Lesson learned: Know your scope – In more details than you think you need

Ask questions, a lot of them. Prepare a checklist for the Sales team and on-boarding process. PMs should make sure to have all of this information in place before the team starts working.

Roses are red, violets are blue
– Clash of personalities

When a new project kicks off, you have to establish a good rapport with a client. Sometimes, when the deadlines are tight and you are already stretched, you don’t have enough time to think about personality match with the client. Things can go south quickly if there is a personality or communication style mismatch that you put under the rug hoping it will sort itself out.

We’ve had a scenario in which our lead developer, who acted as the main point of contact, and the client had almost an outing over a specific task where they poorly communicated the feature request. The confusion, combined with poor judgment on how one can/should communicate directly with a client created enough bad blood to force us to remove the lead developer from all communication with the client and get another team member assume this role.

Fortunately, the timing was not off, and we made it to a point where we managed to salvage the relationship and the project.

Lesson learned: Know your client – Adapt your communication to the client

Keep a close eye on the communication, invest time to learn who you are talking to on the other side, who is the client’s representative, what communication style they prefer, how technical are they… so that you can decide what communication tone and frequency would be the best from your end. In PM-speak, do a stakeholder analysis and use it as input for creating communication matrix for the project.

Six is not enough?

If the six examples did not convince you, we could spend hours talking about:

  • Client thinking something is irrelevant or easy and forgetting to tell us until it’s too late. E.g. automatic order processing is “just one button”, or the client doesn’t mention they have multiple stores,
  • The time we did not contract 3rd party support (for integrated systems),
  • That project where we did not include all stakeholders from the client side (e.g. not all departments) which led to last minute changes and budget overruns,
  • When we did not explain our process to the client, so they were late with their deliveries (e.g. logos, transactional emails),
  • The client that was too detailed and wanted to have spit-polished plans – the whole budget was spent only on planning,
  • Or many projects where we did not check if extensions really work with the latest Magento version (e.g. M1 -> M2 upgrade)
  • Or …

Instead of conclusion

“By failing to prepare, you are preparing to fail.”
– Benjamin Franklin

The six examples we covered are ranging from technical to social, but they have one thing in common. The solution was not technical, it was better processes, communication, organization – in short, better Project Management. They are the best illustration that the biggest risk and the biggest opportunity in projects come from good or bad project management. Please share your experiences in comments – what were the small things that made or broke your projects, and how did you deal with them?

Oh, I almost forgot…

Those skeletons in Vilnius? That wasn’t a mass grave, they were not burying the bodies. The ground was too frozen to dig, so they could not dig trenches. They used frozen corpses of their friends to build a breastwork, a shield, barricades to protect them from advancing Russian army.

Don’t let that happen to you because you ignored small things.

The post Keeping an eye on small things in eCommerce projects appeared first on Inchoo.

]]>
http://inchoo.net/ecommerce/keeping-eye-small-things-ecommerce-projects/feed/ 1
Add custom image field for custom options http://inchoo.net/magento/add-custom-image-field-custom-options/ http://inchoo.net/magento/add-custom-image-field-custom-options/#comments Tue, 24 Oct 2017 10:39:05 +0000 http://inchoo.net/?p=30858 We had a request from a client who wanted to display images for custom options. In this article, I’ll explain how to add the image field to the custom option in admin. Create new module Inchoo_ProductCustomOptionsFile app/etc/modules/Inchoo_ProductCustomOptionsFile.xml <?xml version="1.0"?> <config> <modules> <Inchoo_ProductCustomOptionsFile> <active>true</active> <codePool>local</codePool> </Inchoo_ProductCustomOptionsFile> </modules> </config> Configuration file To add your custom field it...

The post Add custom image field for custom options appeared first on Inchoo.

]]>
We had a request from a client who wanted to display images for custom options. In this article, I’ll explain how to add the image field to the custom option in admin.

Create new module Inchoo_ProductCustomOptionsFile

app/etc/modules/Inchoo_ProductCustomOptionsFile.xml

  1. <?xml version="1.0"?>
    <config>
        <modules>
            <Inchoo_ProductCustomOptionsFile>
                <active>true</active>
                <codePool>local</codePool>
            </Inchoo_ProductCustomOptionsFile>
        </modules>
    </config>

Configuration file

To add your custom field it is necessary to rewrite class         Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Type_Select and set your template. To fill custom option image filed with new image’s names it is necessary to rewrite the class Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Option.

/app/code/local/Inchoo/ProductCustomOptionsFile/etc/config.xml

<?xml version="1.0"?>
<config>
	<modules>
		<Inchoo_ProductCustomOptionsFile>
			<version>1.0.0</version>
		</Inchoo_ProductCustomOptionsFile>
	</modules>
	<global>
		<blocks>
			<adminhtml>
				<rewrite>
					<catalog_product_edit_tab_options_type_select>Inchoo_ProductCustomOptionsFile_Block_Adminhtml_Rewrite_Catalog_Product_Edit_Type_Select</catalog_product_edit_tab_options_type_select>
					<catalog_product_edit_tab_options_option>Inchoo_ProductCustomOptionsFile_Block_Adminhtml_Rewrite_Catalog_Product_Edit_Tab_Options_Option</catalog_product_edit_tab_options_option>
				</rewrite>
			</adminhtml>
			<inchoo_file>
				<class>Inchoo_ProductCustomOptionsFile_Block</class>
			</inchoo_file>
		</blocks>
		<resources>
			<inchoo_productcustomoptionsfile_setup>
				<setup>
					<module>Inchoo_ProductCustomOptionsFile</module>
				</setup>
			</inchoo_productcustomoptionsfile_setup>
		</resources>
	</global>
	<admin>
		<routers>
			<adminhtml>
				<args>
					<modules>
						<inchoo_product before="Mage_Adminhtml">Inchoo_ProductCustomOptionsFile_Adminhtml</inchoo_product>
					</modules>
				</args>
			</adminhtml>
		</routers>
	</admin>
</config>

 Blocks

/app/code/local/Inchoo/ProductCustomOptionsFile/Block/Adminhtml/Rewrite/Catalog/Product/Edit/Type/Select.php

We want to use our template instead default one for custom option.

<?php
class Inchoo_ProductCustomOptionsFile_Block_Adminhtml_Rewrite_Catalog_Product_Edit_Type_Select extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Type_Select
{
	public function __construct()
	{
		parent::__construct();
		$this->setTemplate('catalog/product/edit/options/type/select-with-file.phtml');
		$this->setCanEditPrice(true);
		$this->setCanReadPrice(true);
	}
 
}

Create a template file and copy content of /app/design/adminhtml/default/default/template/catalog/product/edit/options/type/select.phtml into our new template file /app/design/adminhtml/default/default/template/catalog/product/edit/options/type/select-with-file.phtml

Add code between Inchoo into select-with-file.phtml template

<!---...-->
 
OptionTemplateSelect =
<!---...-->
        '<th class="type-sku"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('SKU')) ?></th>'+
            // Inchoo
        '<th class="type-title"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('Image Name')) ?></th>'+
        '<th class="type-title"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('Upload New Image')) ?></th>'+
            // Inchoo
        '<th class="type-title"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('Sort Order')) ?></th>'+
<!---...-->
 
OptionTemplateSelectRow =
<!---...-->
        '<td><input type="text" class="input-text" name="product[options][{{id}}][values][{{select_id}}][sku]" value="{{sku}}"></td>'+
            // Inchoo
        '<td><input type="text" class="select-type-image" id="product_option_{{id}}_select_{{select_id}}_image" name="product[options][{{id}}][values][{{select_id}}][image]" value="{{image}}">{{checkboxScopeTitle}}</td>'+
        '<td><input type="file" class="input-text select-type-image" id="image" name="{{id}}-{{select_id}}"></td>'+
            // Inchoo
        '<td><input type="text" class="validate-zero-or-greater input-text" name="product[options][{{id}}][values][{{select_id}}][sort_order]" value="{{sort_order}}"></td>'+
<!---...-->

To pass our new custom option value to template we need to rewrite Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Option class. Create block /app/code/local/Inchoo/ProductCustomOptionsFile/Block/Adminhtml/Rewrite/Catalog/Product/Edit/Tab/Options/Option.php Add code between Inchoo comment. Function getOptionValues() returns all product custom options to javascript object which fills custom options fields.

class Inchoo_ProductCustomOptionsFile_Block_Adminhtml_Rewrite_Catalog_Product_Edit_Tab_Options_Option extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Option
{
	public function getOptionValues()
	{
 
// ...
	$i = 0;
	$itemCount = 0;
	foreach ($option->getValues() as $_value) {
	/* @var $_value Mage_Catalog_Model_Product_Option_Value */
		$value['optionValues'][$i] = array(
		'item_count' => max($itemCount, $_value->getOptionTypeId()),
		'option_id' => $_value->getOptionId(),
		'option_type_id' => $_value->getOptionTypeId(),
		'title' => $this->escapeHtml($_value->getTitle()),
		'price' => ($showPrice)
		? $this->getPriceValue($_value->getPrice(), $_value->getPriceType()) : '',
		'price_type' => ($showPrice) ? $_value->getPriceType() : 0,
		'sku' => $this->escapeHtml($_value->getSku()),
		'sort_order' => $_value->getSortOrder(),
		// Inchoo
		'image' => $_value->getImage(),
		// Inchoo
		);
// ...

Now we have our custom fields Image Name and Upload New Images :

Saving images

To save images into database, we need to create setup script and rewrite Mage_Adminhtml_Catalog_ProductController. Then create setup script /app/code/local/Inchoo/ProductCustomOptionsFile/sql/inchoo_productcustomoptionsfile_setup/install-1.0.0.php

<?php
/* @var $installer Mage_Core_Model_Resource_Setup */
 
$installer = $this;
 
$installer->getConnection()
->addColumn($installer->getTable('catalog/product_option_type_value'), 'image', 'VARCHAR(255) NULL');
$installer->endSetup();

Rewrite class Mage_Adminhtml_Catalog_ProductController and add code between Inchoo to save images name into table catalog_product_option_type_value and upload images.

 

<?php
require_once(Mage::getModuleDir('controllers','Mage_Adminhtml').DS.'Catalog'.DS.'ProductController.php');
 
class Inchoo_ProductCustomOptionsFile_Adminhtml_Catalog_ProductController extends Mage_Adminhtml_Catalog_ProductController
{
	/**
	 * Initialize product before saving
	 */
	protected function _initProductSave()
	{
 
// ..
 
		$product->setCanSaveConfigurableAttributes(
			(bool) $this->getRequest()->getPost('affect_configurable_product_attributes')
			&& !$product->getConfigurableReadonly()
		);
// Inchoo
		$skuImageName=trim($product->getSku());
		$imagesFiles=$_FILES;
		$path = Mage::getBaseDir('media') . DS . 'catalog' . DS . 'customoption' .DS. 'images';
 
		foreach ($imagesFiles as $key=>$value)
		{
			$optionsValue = explode('-',$key);
			foreach ($value as $key2=>$value2)
			{
				if($key2=='name' && $value2!="") {
					try {
						$uploader = new Varien_File_Uploader($key);
						$uploader->setAllowedExtensions(array('jpg','jpeg','gif','png','svg'));
						$uploader->setAllowRenameFiles(false);
						$uploader->setFilesDispersion(false);
						$optionTitle = trim($productData['options'][$optionsValue[0]]['title']);
						$optionValueTitle = trim($productData['options'][$optionsValue[0]]['values'][$optionsValue[1]]['title']);
						$imageExtension = pathinfo($value2,PATHINFO_EXTENSION);
						$newImageName =$skuImageName.'_'.$optionTitle.'_'.$optionValueTitle.'.'.$imageExtension;
						$uploader->save($path, $newImageName);
						$productData['options'][$optionsValue[0]]['values'][$optionsValue[1]]['image']=$uploader->getUploadedFileName();
					} catch(Exception $e) {
						Mage::log('Unable to save custom option image. ' . $e->getMessage(), null, null, true);
					}
 
				}
			}
		}
// Inchoo

And that’s it. If you have any questions, feel free to post them in comments.

The post Add custom image field for custom options appeared first on Inchoo.

]]>
http://inchoo.net/magento/add-custom-image-field-custom-options/feed/ 1
Solr and Magento – search by department http://inchoo.net/magento/solr-search-by-department/ http://inchoo.net/magento/solr-search-by-department/#comments Wed, 18 Oct 2017 09:29:47 +0000 http://inchoo.net/?p=30692 There are eCommerce stores which sell a wide variety of products like food, personal care, electronics, and so on. On those stores, visitors want to be able to search by a specific category. This can be achieved by adding a new feature: search by department or category. In this quick tutorial I will explain the...

The post Solr and Magento – search by department appeared first on Inchoo.

]]>
There are eCommerce stores which sell a wide variety of products like food, personal care, electronics, and so on. On those stores, visitors want to be able to search by a specific category. This can be achieved by adding a new feature: search by department or category. In this quick tutorial I will explain the base concept of how to do it using Solr search engine as an example.

Assuming you already use Solr search server on your eCommerce site, first step should be checking if there is a category_ids field in Solr index. Category_ids should exist in Solr index in order to be able to filter by category_id.

To check if the field exists, call this url: http://localhost:8983/solr/collection1/select?q=*%3A*&wt=json&indent=true.

You can see category_ids in the response below:

Field category_ids should be declared in Solr schema.xml as in the following example:

        <field name="category_ids"  type="int"  indexed="true" multiValued="true"/>

Category_ids field is multivalued and indexed, can contain more values, and product can be assigned to more categories.

If the previous conditions are fulfilled, only the Magento side should be modified to send a proper category filter with search phrase. If you use Solarium client, it is pretty easy.

$client = new Solarium\Client($config);
 
// get a select query instance
$query = $client->createSelect();
$query->setQuery('Teflon');
$query->setFields(array('id','name','price'));
 
// create a filterquery by category id 3887
$fq = $query->createFilterQuery('category_ids')->setQuery('category_ids:3887');

If you are interested in finding out more about the Solarium concepts, click here.

Finally, query to Solr should look like this:
http://localhost:8983/solr/collection1/magento_en?q=Teflon&fq=category_ids:3887

q parameter is search terms “Teflon”
fq parameter is filter by category id 3887 (more about Solr common parameters)

The results

This is how a “search by category” on Magento frontend can look like:

With this feature you can improve site search performance and decrease the need for search refinement which, ultimately, has direct impact on your eCommerce conversion rate.

The post Solr and Magento – search by department appeared first on Inchoo.

]]>
http://inchoo.net/magento/solr-search-by-department/feed/ 1
How to keep design library in sync across the team? Welcome Sketch Libraries! http://inchoo.net/magento/design/how-to-keep-design-library-in-sync-across-the-team-welcome-sketch-libraries/ http://inchoo.net/magento/design/how-to-keep-design-library-in-sync-across-the-team-welcome-sketch-libraries/#respond Tue, 17 Oct 2017 10:43:10 +0000 http://inchoo.net/?p=30680 The buzz these days is all about design systems, but design system by itself is not enough to ensure consistency through all designs. When working with design systems, the main challenges are ongoing maintenance and informing everyone about the changes. For a long time, there wasn’t a thorough solution for designers who design in Sketch...

The post How to keep design library in sync across the team? Welcome Sketch Libraries! appeared first on Inchoo.

]]>
The buzz these days is all about design systems, but design system by itself is not enough to ensure consistency through all designs. When working with design systems, the main challenges are ongoing maintenance and informing everyone about the changes.

For a long time, there wasn’t a thorough solution for designers who design in Sketch which would provide easy access to the latest styles and propagate changed assets to team members. Yeah, we had the ability to share symbols via plugins for a while (Craft’s Library), but there were too many problems, and sharing library is too important to rely on a third-party plugin.

Welcome Sketch Libraries

Sketch just made public the Sketch 47, and we finally have a document with symbols which can be used across other documents, so let’s see how to use libraries in Sketch.

1. Create a Sketch document with at least one symbol and save your document in Dropbox, Box, Sync or any other place where your colleagues have access.

2. Press CMD + comma to open Sketch’s Preferences and navigate to the Libraries tab.

3. You’ll notice there is iOS UI Design library included, but we’ll create a new library. Click on the “Add Library…” button and choose your document. Congrats, you’ve just created a single source of truth for everyone in your team.

Team members can now easily add the Library by following the same steps mentioned above and access the symbols in that file from any Sketch file.

Inserting, editing and accepting changes

Using shared library is simple and straightforward. Inserting symbols works just like inserting regular symbols, the only difference is they are not placed in your document. To insert a symbol just find your shared library at the bottom of the list on the Insert menu.

You’ll notice external symbols have slightly different icons from the local symbols to avoid confusion.

Once inserted, there are two options for editing an external symbol. You can unlink it from Library or open it in the Original document.


If you choose “Unlink from Library”, it will detach from the external library and become a local symbol in your current Sketch file.

Making changes in the original document will affect all instances of the symbol across any document which is using this library, but only if those changes are accepted. After making changes, everyone who is using this library will see “Library Update Available” badge on the top-right corner of Sketch.

Maybe Sketch crew should make that badge more prominent because it’s easy to miss. Anyhow, clicking on it will display a dialog box with outdated symbols and an option to selectively update them.

To sum up…

This feature is definitely a game changer for all Sketch users and it will change real-time collaboration permanently. What we would like to see in some of the future updates is an option to include text styles and layer styles in a library.

The post How to keep design library in sync across the team? Welcome Sketch Libraries! appeared first on Inchoo.

]]>
http://inchoo.net/magento/design/how-to-keep-design-library-in-sync-across-the-team-welcome-sketch-libraries/feed/ 0
Case Study: Migration from Magento 1 to Magento 2 for Sloan Express http://inchoo.net/magento-2/case-study-migration-magento-1-magento-2-sloan-express/ http://inchoo.net/magento-2/case-study-migration-magento-1-magento-2-sloan-express/#respond Mon, 02 Oct 2017 11:54:40 +0000 http://inchoo.net/?p=30654 Sloan Express is a family-owned business with deep roots in the agricultural industry that have been serving the needs of farmers worldwide for over 80 years. Located in Central Illinois, Sloan Express is the area leader offering new agricultural parts that are equal to or better than the original equipment part. They sell directly to...

The post Case Study: Migration from Magento 1 to Magento 2 for Sloan Express appeared first on Inchoo.

]]>

Sloan Express is a family-owned business with deep roots in the agricultural industry that have been serving the needs of farmers worldwide for over 80 years. Located in Central Illinois, Sloan Express is the area leader offering new agricultural parts that are equal to or better than the original equipment part. They sell directly to farmers, implement dealers and repair shops.

Sloan Express has been able to address some of the problems that today’s farmers face: parts not stocked locally; shortage of local sources for parts; and most important – TIME. The Sloan Express Customer prompt and direct delivery with no time wasted trying to find a part and then waiting for it to come in.

What were the challenges for us?

Sloanex_new2017
Sloanex_old2016

Recognizing the need to take the business to the next level, Jeff Sloan and the team from Sloan Express approached us looking for Magento professionals that can migrate their existing store from 1.7. Open Source edition to Magento 2.

Magento 2 introduces new methodologies and technologies for delivering enhanced shopping and store experience to the merchants and users. But to be honest, migrating from Magento 1 to Magento 2 is not an easy and trouble-free process. Since it’s not automated, there is plenty of manual work that needs to be done by professionals who understand migration process and your business in order to get a stable and fully functional store.

Sloan Express knew what they wanted for their future. They required a solution that can easily scale up if required and has a modular architecture to ensure faster page load time, faster add-to-cart server response time and faster end-to-end checkout time.

Inchoo at Sloan Express

Inchoo at Sloan Express

Magento 2 Open source (previously known as Community Edition) comes with support for only MySQL search engine, but some projects require better or more adjustable search engine in order to increase sales or conversion rate. For Sloan Express, we’ve implemented SOLR search engine in order to achieve blazing-fast search results that are also highly reliable and fault tolerant. With a near real-time indexing, advanced full-text search capabilities and optimisation for high volume traffic, SOLR has brought a new dimension for the customers using the site.

The client wanted to keep all of their existing features and extensions from Magento 1. As one can imagine, Magento 1 extensions are not compatible with Magento 2, so we implemented new ones and set fundamentals for all future technical implementations and integrations as PIM, ERP and other complex technical systems.

Since Sloan Express had a large number of categories, we had to do a major restructuring of the store’s hierarchy, which, among other, resulted with structured navigation and flow that seems more natural to the end user.

The end result

Sloan Express now has modern and clean responsive design with a completely new look and flow that provides optimal viewing and interaction experience across a wide range of devices. We designed it by having in mind business needs for this special niche and eCommerce trends supported with the gathered information and behavior of visitors on the previous store.

Results Sloanex Inchoo

Implementing several analytic tools, we now have a better understanding of the customer’s journey and how they engage with the brand. That gives us the opportunity to continuously test and improve technical functionalities and user experience in order to increase revenue and reflect the quality that stands behind the name of Sloan Express in agriculture world.

The post Case Study: Migration from Magento 1 to Magento 2 for Sloan Express appeared first on Inchoo.

]]>
http://inchoo.net/magento-2/case-study-migration-magento-1-magento-2-sloan-express/feed/ 0
Programmatically create upsell, cross sell and related products in Magento http://inchoo.net/magento/programmatically-create-upsell-crosssell-related-products-magento/ http://inchoo.net/magento/programmatically-create-upsell-crosssell-related-products-magento/#comments Thu, 21 Sep 2017 08:52:12 +0000 http://inchoo.net/?p=30529 This article will explain how to add upsell, cross sell and related products programmatically to Magento. One of practical examples would be data migration from some other ecommerce system to Magento. You can read a nice article on how to add upsell, cross sell and related products from administration here. It explains what all these...

The post Programmatically create upsell, cross sell and related products in Magento appeared first on Inchoo.

]]>
This article will explain how to add upsell, cross sell and related products programmatically to Magento. One of practical examples would be data migration from some other ecommerce system to Magento. You can read a nice article on how to add upsell, cross sell and related products from administration here. It explains what all these product relations mean and where are they used on the site.

Load existing product data

At the beginning, there is a product that need to be updated with product relations. It needs to be loaded as usual.

$product = Mage::getModel('catalog/product')->load($productId);

This loaded product model will not contain information about already existing upsell, cross sell and related products. If loaded product doesn’t have previous upsell, cross sell or related products set, it can be saved immediately with new data. But, if there is already existing data about these products, it must be loaded first, merged with new data and then saved. There are specific functions for that. Otherwise, it would be overwritten with new data only.

$upSellProducts = $product->getUpSellProducts();
$crossSellProducts = $product->getCrossSellProducts();
$relatedProducts = $product->getRelatedProducts();

These functions load all upsell, cross sell and related product models as an array with numeric keys starting from zero.

 

Prepare existing product data

In order to update product’s upsell, cross sell and related information, they need to be rearranged in array with product ids as keys. This array should also contain information about product position as a subarray. Position parameter determines product’s order position on frontend, usually in sidebar or slider. This parameter can also be set through Magento administration by opening product’s upsell, cross sell or related tab.

foreach ($upSellProducts as $upSellProduct) {
    $upSellProductsArranged[$upSellProduct->getId()] = array('position' => $$upSellProduct->getPosition());
}
 
foreach ($crossSellProducts as $crossSellProduct) {
    $crossSellProductsArranged[$crossSellProduct->getId()] = array('position' => $crossSellProduct->getPosition());
}
 
foreach ($relatedProducts as $relatedProduct) {
    $relatedProductsArranged[$relatedProduct->getId()] = array('position' => $relatedProduct->getPosition());
}

Merge new product data

When migrating products to Magento, products will be created, not updated, so if there is multiple upsell, cross sell or related products, this parameter can be incremented in a loop starting from zero or they can all be set to zero.Merging new upsell, cross sell and related products:

$newUpSellProducts = array($newUpSellProduct1, $newUpSellProduct2);
foreach ($newUpSellProducts as $newUpSellProduct) {
    $upSellProductsArranged[$newUpSellProduct->getId()] = array('position' => '');
}
 
$newCrossSellProducts = array($newCrossSellProduct1, $newCrossSellProduct2);
foreach ($newCrossSellProducts as $newCrossSellProduct) {
    $crossSellProductsArranged[$newCrossSellProduct->getId()] = array('position' => '');
}
 
$newRelatedProducts = array($newRelatedProduct1, $newRelatedProduct2);
foreach ($newRelatedProducts as $newRelatedProduct) {
    $relatedProductsArranged[$newRelatedProduct->getId()] = array('position' => '');
}

When all relations are merged, they should be set as one of product’s _data parameter:

$product->setUpSellLinkData($upSellProductsArranged);
$product->setCrossSellLinkData($crossSellProductsArranged);
$product->setRelatedLinkData($relatedProductsArranged);

Finally the product can be saved:

$product->save();

Database structure

This may seem like a simple thing. All that is needed is to set upsell, cross sell and related product ids in array with their positions and save the product. Backend process is actually complicated. Magento function that handles product relations saving process is saveProductRelations($product). It is located in Mage_Catalog_Model_Product_Link class.

Database structure for product relations is eav structure. Main table, in which most of this information is saved, is “catalog_product_link”. It’s structure is very simple. It consists of 4 columns. “link_id” is increment ID, “product_id” is edited product, “linked_product_id” is ID of the product that is related to edited product, “link_type_id” is relation type ID. 4 is for upsell, 5 for cross sell and 1 for related product. Second table worth mentioning is “catalog_product_link_attribute_int” which saves product’s position parameter mentioned earlier.

 

select * from catalog_product_link;

+---------+------------+-------------------+--------------+
| link_id | product_id | linked_product_id | link_type_id |
+---------+------------+-------------------+--------------+
|     1   |     247    |               640 |            1 |
|     2   |     247    |               642 |            1 |
|     3   |     247    |               647 |            1 |
|     4   |     247    |               641 |            1 |
|     5   |     247    |               652 |            4 |
|     6   |     247    |               651 |            4 |
|     7   |     247    |               651 |            5 |
|     8   |     247    |               652 |            5 |
|     9   |     247    |               652 |            1 |
|    10   |     247    |               651 |            1 |
+---------+------------+-------------------+--------------+

The post Programmatically create upsell, cross sell and related products in Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/programmatically-create-upsell-crosssell-related-products-magento/feed/ 1
Adding gallery tab to a CMS page http://inchoo.net/magento/adding-gallery-tab-cms-page/ http://inchoo.net/magento/adding-gallery-tab-cms-page/#comments Tue, 29 Aug 2017 10:53:32 +0000 http://inchoo.net/?p=29908 In this article I will demonstrate how you can create new tab into admin cms page so you can create image gallery similar as upload images for product. Step 1 Register your module : <config> <modules> <Inchoo_Gallery> <active>1</active> <codePool>local</codePool> </Inchoo_Gallery> </modules> </config> Step 2 Module configuration : <config> <modules> <Inchoo_Gallery> <version>1.0.0</version> </Inchoo_Gallery> </modules> </config> Step...

The post Adding gallery tab to a CMS page appeared first on Inchoo.

]]>
In this article I will demonstrate how you can create new tab into admin cms page so you can create image gallery similar as upload images for product.

Step 1

Register your module :

<config>
	<modules>
		<Inchoo_Gallery>
			<active>1</active>
			<codePool>local</codePool>
		</Inchoo_Gallery>
	</modules>
</config>

Step 2

Module configuration :

<config>
	<modules>
		<Inchoo_Gallery>
			<version>1.0.0</version>
		</Inchoo_Gallery>
	</modules>
</config>

Step 3

We need to store images into database so we need to create a model, collection, resource and setup script.Our model will implement JsonSerializable so we need to implement jsonSerialize method which will return required json data about the model.

Config:

<!--....-->
<global>
 <models>
  <inchoo_gallery>
   <class>Inchoo_Gallery_Model</class>
    <resourceModel>inchoo_gallery_resource</resourceModel>
   </inchoo_gallery>
   <inchoo_gallery_resource>
    <class>Inchoo_Gallery_Model_Resource</class>
    <entities>
     <gallery>
      <table>inchoo_cms_gallery</table>
     </gallery>
    </entities>
    </inchoo_gallery_resource>
     </models>
     <resources>
      <inchoo_gallery_setup>
       <setup>
        <module>Inchoo_Gallery</module>
       </setup>
      </inchoo_gallery_setup>
     </resources>
</global>
<!--....-->

Model:

class Inchoo_Gallery_Model_Gallery extends
Mage_Core_Model_Abstract implements JsonSerializable {
 public function _construct() {
  $this->_init( 'inchoo_gallery/gallery' );
	}
 
 public function jsonSerialize() {
  return [
    'id'               => $this->getId(),
    'file'             => $this->getFile(),
    'label'            => $this->getLabel(),
    'position'         => $this->getPosition(),
    'disabled'         => $this->getIsDisabled(),
    'label_default'    => $this->getLabel(),
    'position_default' => $this->getPosition(),
    'disabled_default' => $this->getIsDisabled(),
    'url'              => Mage::getBaseUrl( Mage_Core_Model_Store::URL_TYPE_WEB ) . 'media/gallery/' . $this->getFile(),
    'removed'          => 0
		];
	}
}

Resource:

class Inchoo_Gallery_Model_Resource_Gallery
extends Mage_Core_Model_Resource_Db_Abstract
{
	public function _construct() {
		$this->_init('inchoo_gallery/gallery','id');
	}
}

Collection:

<?php
class Inchoo_Gallery_Model_Resource_Gallery_Collection
extends Mage_Core_Model_Resource_Db_Collection_Abstract
{
	public function _construct() {
		$this->_init('inchoo_gallery/gallery');
	}
}

Setup script:

<?php
$installer = $this;
$installer->startSetup();
 
$table = $installer->getConnection()
                   ->newTable( $installer->getTable( 'inchoo_gallery/gallery' ) )
                   ->addColumn( 'id', Varien_Db_Ddl_Table::TYPE_INTEGER, null,
	                   array(
		                   'identity' => true,
		                   'unsigned' => true,
		                   'nullable' => false,
		                   'primary'  => true,
	                   ), 'Value id' )
                   ->addColumn( 'cms_page_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null,
	                   array(
		                   'nullable' => false,
	                   ), 'Cms Page id' )
                   ->addColumn( 'position', Varien_Db_Ddl_Table::TYPE_INTEGER, null,
	                   array(
		                   'unsigned' => true,
		                   'nullable' => true,
	                   ), 'Position' )
                   ->addColumn( 'file', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255,
	                   array(
		                   'nullable' => false,
	                   ), 'File Name' )
                   ->addColumn( 'label', Varien_Db_Ddl_Table::TYPE_VARCHAR, 255,
	                   array(
		                   'nullable' => true,
	                   ), 'Label' )
                   ->addColumn( 'is_disabled', Varien_Db_Ddl_Table::TYPE_BOOLEAN, null,
	                   array(
		                   'nullable' => true,
	                   ), 'Is Disabled' );
 
$installer->getConnection()->createTable( $table );
 
$installer->getConnection()->addForeignKey(
	$installer->getFkName(
		'inchoo_gallery/gallery',
		'cms_page_id',
		'cms/page',
		'page_id'
	),
	$installer->getTable( 'inchoo_gallery/gallery' ),
	'cms_page_id',
	$installer->getTable( 'cms/page' ),
	'page_id'
);
$installer->endSetup();

Step 4

Next we need to create our block and template file.

Config:

<!--....-->
 
<global>
<!--....-->
		<blocks>
			<inchoo_gallery>
				<class>Inchoo_Gallery_Block</class>
			</inchoo_gallery>
		</blocks>
</global>
<!--....-->

Create block:

<?php
class Inchoo_Gallery_Block_Adminhtml_Gallery
	extends Mage_Adminhtml_Block_Widget
	implements Mage_Adminhtml_Block_Widget_Tab_Interface {
	protected $_uploaderType = 'uploader/multiple';
 
	public function __construct() {
		parent::__construct();
		$this->setShowGlobalIcon( true );
		Mage_Adminhtml_Block_Template::__construct();
		$this->setTemplate( 'inchoo/gallery.phtml' );
	}
 
	protected function _prepareLayout() {
		$this->setChild( 'uploader',
			$this->getLayout()->createBlock( $this->_uploaderType )
		);
 
		$this->getUploader()->getUploaderConfig()
		     ->setFileParameterName( 'image' )
		     ->setTarget( Mage::getModel( 'adminhtml/url' )->addSessionParam()->getUrl( '*/cms_page/upload' ) );
 
		$browseConfig = $this->getUploader()->getButtonConfig();
		$browseConfig
			->setAttributes( array(
				'accept' => $browseConfig->getMimeTypesByExtensions( 'gif, png, jpeg, jpg' )
			) );
 
		return Mage_Adminhtml_Block_Template::_prepareLayout();
	}
 
	public function getUploader() {
		return $this->getChild( 'uploader' );
	}
 
	public function getUploaderHtml() {
		return $this->getChildHtml( 'uploader' );
	}
 
	public function getHtmlId() {
		return 'media_gallery_content';
	}
 
	public function getGallery() {
		return Mage::getModel( 'inchoo_gallery/gallery' )->getCollection()
		           ->addFieldToFilter( 'cms_page_id', array( 'eq' => $this->getRequest()->getParam( 'page_id' ) ) );
 
	}
 
	public function getImageTypes() {
		return array( 'image' => [ 'label' => 'Base Image', 'field' => 'post[image]' ] );
	}
 
	public function getImageTypesJson() {
		return Mage::helper( 'core' )->jsonEncode( $this->getImageTypes() );
	}
 
	public function getJsObjectName() {
		return 'media_gallery_contentJsObject';
	}
 
	public function getImagesJson() {
 
		$jsonFiles = '';
		$gallery   = $this->getGallery();
		foreach ( $gallery as $images ) {
			$jsonFiles = $jsonFiles . ',' . json_encode( $images );
		}
 
		return '[' . trim( $jsonFiles, ',' ) . ']';
	}
 
	/**
	 * Prepare label for tab
	 *
	 * @return string
	 */
	public function getTabLabel() {
		return Mage::helper( 'cms' )->__( 'Gallery' );
	}
 
	/**
	 * Prepare title for tab
	 *
	 * @return string
	 */
	public function getTabTitle() {
		return Mage::helper( 'cms' )->__( 'Gallery' );
	}
 
	/**
	 * Returns status flag about this tab can be showen or not
	 *
	 * @return true
	 */
	public function canShowTab() {
		return true;
	}
 
	/**
	 * Returns status flag about this tab hidden or not
	 *
	 * @return true
	 */
	public function isHidden() {
		return false;
	}
 
	/**
	 * Check permission for passed action
	 *
	 * @param string $action
	 *
	 * @return bool
	 */
	protected function _isAllowedAction( $action ) {
		return true;
	}
 
}

Create template file :

Create template. This template is based on gallery.phtml which is used in Mage_Adminhtml_Block_Catalog_Product_Helper_Form_Gallery_Content block. I modified this template to fit for our purpose.

<?php
/* @var $this Inchoo_Gallery_Block_Adminhtml_Gallery */
?>
 
<div class="entry-edit-head">
    <h4 class="icon-head head-edit-form fieldset-legend">Images</h4>
    <div class="form-buttons"></div>
</div>
<div class="fieldset fieldset-wide" id="<?php echo $this->getHtmlId() ?>">
    <div class="hor-scroll">
        <table class="form-list" style="width: 100%;" cellspacing="0">
            <tbody>
            <tr>
                <td class="value" colspan="3" style="width: 100%;">
                    <div id="<?php echo $this->getHtmlId() ?>">
                        <ul class="messages">
                            <li class="notice-msg">
                                <ul>
                                    <li>
										<?php echo Mage::helper( 'catalog' )->__( 'Image type and information need to be specified for each store view.' ); ?>
                                    </li>
                                </ul>
                            </li>
                        </ul>
                        <div class="grid">
                            <table cellspacing="0" class="data border" id="<?php echo $this->getHtmlId() ?>_grid"
                                   width="100%">
                                <col width="1"/>
                                <col/>
                                <col width="70"/>
								<?php foreach ( $this->getImageTypes() as $typeId => $type ): ?>
                                    <col/>
								<?php endforeach; ?>
                                <col width="70"/>
                                <col width="70"/>
                                <thead>
                                <tr class="headings">
                                    <th><?php echo Mage::helper( 'catalog' )->__( 'Image' ) ?></th>
                                    <th><?php echo Mage::helper( 'catalog' )->__( 'Label' ) ?></th>
                                    <th><?php echo Mage::helper( 'catalog' )->__( 'Sort Order' ) ?></th>
									<?php foreach ( $this->getImageTypes() as $typeId => $type ): ?>
                                        <th><?php echo $type['label'] ?></th>
									<?php endforeach; ?>
                                    <th><?php echo Mage::helper( 'catalog' )->__( 'Exclude' ) ?></th>
                                    <th class="last"><?php echo Mage::helper( 'catalog' )->__( 'Remove' ) ?></th>
                                </tr>
                                </thead>
                                <tbody id="<?php echo $this->getHtmlId() ?>_list">
                                <tr id="<?php echo $this->getHtmlId() ?>_template" class="template no-display">
                                    <td class="cell-image">
                                        <div class="place-holder"
                                             onmouseover="<?php echo $this->getJsObjectName(); ?>.loadImage('__file__')">
                                            <span><?php echo Mage::helper( 'catalog' )->__( 'Roll Over for preview' ) ?></span>
                                        </div>
                                        <img src="<?php echo $this->getSkinUrl( 'images/spacer.gif' ) ?>" width="100"
                                             style="display:none;" alt=""/></td>
                                    <td class="cell-label"><input type="text" class="input-text"
                                                                  onkeyup="<?php echo $this->getJsObjectName(); ?>.updateImage('__file__')"
                                                                  onchange="<?php echo $this->getJsObjectName(); ?>.updateImage('__file__')"/>
                                    </td>
                                    <td class="cell-position"><input type="text" class="input-text validate-number"
                                                                     onkeyup="<?php echo $this->getJsObjectName(); ?>.updateImage('__file__')"
                                                                     onchange="<?php echo $this->getJsObjectName(); ?>.updateImage('__file__')"/>
                                    </td>
									<?php foreach ( $this->getImageTypes() as $typeId => $type ): ?>
                                        <td class="cell-<?php echo $typeId ?> a-center"><input type="radio"
                                                                                               name="<?php echo $type['field'] ?>"
                                                                                               onclick="<?php echo $this->getJsObjectName(); ?>.setProductImages('__file__')"
                                                                                               value="__file__"/></td>
									<?php endforeach; ?>
                                    <td class="cell-disable a-center"><input type="checkbox"
                                                                             onclick="<?php echo $this->getJsObjectName(); ?>.updateImage('__file__')"/>
                                    </td>
                                    <td class="cell-remove a-center last"><input type="checkbox"
                                                                                 onclick="<?php echo $this->getJsObjectName(); ?>.updateImage('__file__')"/>
                                    </td>
                                </tr>
                                <tr id="<?php echo $this->getHtmlId() ?>-image-0">
                                    <td class="cell-image"><?php echo Mage::helper( 'catalog' )->__( 'No image' ) ?></td>
                                    <td class="cell-label"><input type="hidden"/>&nbsp;</td>
                                    <td class="cell-position"><input type="hidden"/>&nbsp;</td>
									<?php foreach ( $this->getImageTypes() as $typeId => $type ): ?>
                                        <td class="cell-<?php echo $typeId ?> a-center"><input type="radio"
                                                                                               name="<?php echo $type['field'] ?>"
                                                                                               onclick="<?php echo $this->getJsObjectName(); ?>.setProductImages('no_selection')"
                                                                                               value="no_selection"/>
                                        </td>
									<?php endforeach; ?>
                                    <td class="cell-disable"><input type="hidden"/>&nbsp;</td>
                                    <td class="cell-remove last"><input type="hidden"/>&nbsp;</td>
                                </tr>
                                </tbody>
                                <tfoot>
                                <tr>
                                    <td colspan="100" class="last" style="padding:8px">
										<?php echo Mage::helper( 'catalog' )->__( 'Maximum width and height dimension for upload image is %s.', Mage::getStoreConfig( Mage_Catalog_Helper_Image::XML_NODE_PRODUCT_MAX_DIMENSION ) ); ?>
										<?php echo $this->getUploaderHtml() ?>
                                    </td>
                                </tr>
                                </tfoot>
                            </table>
                        </div>
                    </div>
                    <input type="hidden" id="<?php echo $this->getHtmlId() ?>_save" name="post[media_gallery][images]"
                           value="<?php echo $this->escapeHtml( $this->getImagesJson() ) ?>"/>
                    <input type="hidden" id="<?php echo $this->getHtmlId() ?>_save_image"
                           name="post[media_gallery][values]" value="{}"/>
                    <script type="text/javascript">
                        //<![CDATA[
                        var <?php echo $this->getJsObjectName(); ?> =
                        new Product.Gallery('<?php echo $this->getHtmlId() ?>', <?php echo $this->getImageTypesJson() ?>);
                        //]]>
                    </script>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
</div>

Step 5

To add our block as new gallery tab we need to create new layout file and use existing action handle and reference to block which contain all tabs blocks in cms page.

<layout>
	<adminhtml_cms_page_edit>
		<reference name="cms_page_edit_tabs">
			<block type="inchoo_gallery/adminhtml_gallery" name="cms_page_edit_tab_gallery"/>
			<action method="addTab">
				<name>gallery_section</name>
				<block>cms_page_edit_tab_gallery</block>
			</action>
		</reference>
	</adminhtml_cms_page_edit>
</layout>

Step 6

Finally we need to add logic for saving our gallery into database. We need to override Mage_Adminhtml_Cms_PageController and create our logic for saving and upload the data.

Config:

<config>
<!--....-->
	<admin>
		<routers>
			<adminhtml>
				<args>
					<modules>
						<inchoo_gallery before="Mage_Adminhtml">Inchoo_Gallery_Adminhtml_Rewrite</inchoo_gallery>
					</modules>
				</args>
			</adminhtml>
 
		</routers>
	</admin>
<!--....-->
</config>

Create controller:

<?php
require_once Mage::getModuleDir( 'controllers', 'Mage_Adminhtml' ) . DS . 'Cms' . DS . 'PageController.php';
 
class Inchoo_Gallery_Adminhtml_Rewrite_Cms_PageController extends Mage_Adminhtml_Cms_PageController {
 
	public function saveAction(){
	//Add this code after $model->save
 
				$request    = $this->getRequest()->getPost();
				$formImages = json_decode( $request['post']['media_gallery']['images'] );
 
				foreach ( $formImages as $galleryImage ) {
 
					//if new image is uploaded
					if ( ! isset( $galleryImage->id ) ) {
 
						if ( $galleryImage->removed == 1 ) {
							$filePath = Mage::getBaseDir( 'media' ) . DS . 'gallery' . DS . $galleryImage->file;
							if ( file_exists( $filePath ) ) {
								unlink( $filePath );
							}
 
						} else {
							$galleryModel = Mage::getModel( 'inchoo_gallery/gallery' );
 
							$galleryModel->setCmsPageId( $model->getId() );
							$galleryModel->setFile( $galleryImage->file );
							$galleryModel->setPosition( $galleryImage->position );
							$galleryModel->setLabel( $galleryImage->label );
							$galleryModel->setIsDisabled( $galleryImage->disabled );
 
							$galleryModel->save();
						}
 
					}
 
					if ( isset( $galleryImage->id ) ) {
						if ( $galleryImage->removed == 1 ) {
							$filePath = Mage::getBaseDir( 'media' ) . DS . 'gallery' . DS . $galleryImage->file;
 
							$galleryModel = Mage::getModel( 'inchoo_gallery/gallery' );
							$galleryModel->setId( $galleryImage->id );
							$galleryModel->delete();
 
							if ( file_exists( $filePath ) ) {
								unlink( $filePath );
							}
 
						} else {
 
							$isModified = false;
 
							if ( $galleryImage->label_default != $galleryImage->label ) {
								$isModified = true;
							}
 
							if ( $galleryImage->position_default != $galleryImage->position ) {
								$isModified = true;
							}
 
							if ( $galleryImage->disabled_default != $galleryImage->disabled ) {
								$isModified = true;
							}
 
							if ( $isModified ) {
								$galleryModel = Mage::getModel( 'inchoo_gallery/gallery' );
								$galleryModel->setId( $galleryImage->id );
								$galleryModel->setPosition( $galleryImage->position );
								$galleryModel->setIsDisabled( $galleryImage->disabled );
								$galleryModel->setLabel( $galleryImage->label );
 
								$galleryModel->save();
 
							}
						}
 
					}
 
				}
 
	//other code
       }
	public function uploadAction() {
		try {
			$uploader = new Varien_File_Uploader( 'image' );
			$uploader->setAllowedExtensions( array( 'jpg', 'jpeg', 'gif', 'png' ) );
			$uploader->setAllowRenameFiles( true );
			$uploader->setFilesDispersion( false );
			$path   = Mage::getBaseDir( 'media' ) . DS . 'gallery';
			$result = $uploader->save( $path );
			/**
			 * Workaround for prototype 1.7 methods "isJSON", "evalJSON" on Windows OS
			 */
			$result['tmp_name'] = str_replace( DS, "/", $result['tmp_name'] );
			$result['path']     = str_replace( DS, "/", $result['path'] );
 
			$result['url']    = Mage::getBaseUrl( Mage_Core_Model_Store::URL_TYPE_WEB ) . 'media/gallery/' . $result['name'];
			$result['cookie'] = array(
				'name'     => session_name(),
				'value'    => $this->_getSession()->getSessionId(),
				'lifetime' => $this->_getSession()->getCookieLifetime(),
				'path'     => $this->_getSession()->getCookiePath(),
				'domain'   => $this->_getSession()->getCookieDomain()
			);
 
		} catch ( Exception $e ) {
			$result = array(
				'error'     => $e->getMessage(),
				'errorcode' => $e->getCode()
			);
		}
		$this->getResponse()->setBody( Mage::helper( 'core' )->jsonEncode( $result ) );
	}
 
}

And that’s it! Happy coding!

The post Adding gallery tab to a CMS page appeared first on Inchoo.

]]>
http://inchoo.net/magento/adding-gallery-tab-cms-page/feed/ 4
Restrict website access – require log in http://inchoo.net/magento/restrict-website-access-require-log/ http://inchoo.net/magento/restrict-website-access-require-log/#respond Wed, 23 Aug 2017 07:25:29 +0000 http://inchoo.net/?p=30351 There might be times when you do not want your catalog to be publicly visible, especially if running a B2B shop. Reasons for this are numerous but, from our experience, prices are on top of the list. Unfortunately, when it comes to Magento (at least Open Source edition), this feature is not available out of...

The post Restrict website access – require log in appeared first on Inchoo.

]]>
There might be times when you do not want your catalog to be publicly visible, especially if running a B2B shop. Reasons for this are numerous but, from our experience, prices are on top of the list. Unfortunately, when it comes to Magento (at least Open Source edition), this feature is not available out of the box. Lucky for us, implementation is pretty straightforward. Let’s dig in.

The idea

When a customer tries to access the store (catalog, CMS, checkout, etc.), redirect him to login form (if he is not already logged in).

The plan

When planning this feature, good thing to know about Magento is that every route is composed of 3 parts:

  1. Module name
  2. Controller name
  3. Action name

We can use this piece of information on every request that comes to Magento, in order to see where the request is supposed to go.

This is only half of the information we need. Additionally, we need to know whether customer is already logged in.

Specific combination of conditions above should redirect the customer to login form.

What would be the proper way to check for those conditions? Well, hooking into Magento’s dispatching process (namely, predispatch), we can create an observer that will check what route is requested, as well as whether customer is already logged in.

  1. Create a 3rd party module (e.g. Inchoo_WebsiteRestriction)
  2. Register an observer
  3. Implement logic
The execution
  1. Done
  2. create file in Inchoo/WebsiteRestriction/etc/frontend/events.xml
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
        <event name="controller_action_predispatch">
            <observer name="restrict_website" instance="Inchoo\WebsiteRestriction\Observer\RestrictWebsite" />
        </event>
    </config>
  3. Create observer file at Inchoo/WebsiteRestriction/Observer/RestrictWebsite.php
    <?php
     
    namespace Inchoo\WebsiteRestriction\Observer;
     
    use Magento\Customer\Model\Context;
    use Magento\Framework\Event\Observer;
    use Magento\Framework\Event\ObserverInterface;
    use Magento\Store\Model\StoreManagerInterface;
     
    class RestrictWebsite implements ObserverInterface
    {
     
        /**
         * RestrictWebsite constructor.
         */
        public function __construct(
            \Magento\Framework\Event\ManagerInterface $eventManager,
            \Magento\Framework\App\Response\Http $response,
            \Magento\Framework\UrlFactory $urlFactory,
            \Magento\Framework\App\Http\Context $context,
            \Magento\Framework\App\ActionFlag $actionFlag
        )
        {
            $this->_response = $response;
            $this->_urlFactory = $urlFactory;
            $this->_context = $context;
            $this->_actionFlag = $actionFlag;
        }
     
        /**
         * @param Observer $observer
         * @return void
         */
        public function execute(Observer $observer)
        {
            $allowedRoutes = [
                'customer_account_login',
                'customer_account_loginpost',
                'customer_account_create',
                'customer_account_createpost',
                'customer_account_logoutsuccess',
                'customer_account_confirm',
                'customer_account_confirmation',
                'customer_account_forgotpassword',
                'customer_account_forgotpasswordpost',
                'customer_account_createpassword',
                'customer_account_resetpasswordpost',
                'customer_section_load'
            ];
     
            $request = $observer->getEvent()->getRequest();
            $isCustomerLoggedIn = $this->_context->getValue(Context::CONTEXT_AUTH);
            $actionFullName = strtolower($request->getFullActionName());
     
            if (!$isCustomerLoggedIn && !in_array($actionFullName, $allowedRoutes)) {
                $this->_response->setRedirect($this->_urlFactory->create()->getUrl('customer/account/login'));
            }
     
        }
    }

Following the execute method, we have a list of routes that are white-listed always (e.g. login, logout, create account, reset password, etc.). Reason for this is that customer should be able to access those routes regardless of him being logged in or not (i.e. there is no need to redirect customer to login form, if he is already trying to access that page).

In addition to inspecting the correct route, we need to check whether customer is already logged in. There are at least 2 ways to check this, and one of them is implemented above. The other one would be to use Magento’s customer session object (\Magento\Customer\Model\Session) and its isLoggedIn() method. However, according to this stackexchange question, there are some issues with it, so we’re not using it this time.

The end

To conclude, if a customer is not logged in, he is redirected to login form where he can login/register. Using this, we restrict access to the store’s catalog and CMS pages.

The post Restrict website access – require log in appeared first on Inchoo.

]]>
http://inchoo.net/magento/restrict-website-access-require-log/feed/ 0
How to run a quick security check of your Magento store? http://inchoo.net/magento/quick-security-check-magento-store/ http://inchoo.net/magento/quick-security-check-magento-store/#comments Wed, 12 Jul 2017 09:34:24 +0000 http://inchoo.net/?p=29983 Security of any software system, let alone an eCommerce one, is becoming one of the hottest topics out there. How can you as a store owner do a free and quick security check of your Magento website, even without immediate development assistance? Read on and make sure to stay secure! Magento has a vibrant ecosystem...

The post How to run a quick security check of your Magento store? appeared first on Inchoo.

]]>
Security of any software system, let alone an eCommerce one, is becoming one of the hottest topics out there. How can you as a store owner do a free and quick security check of your Magento website, even without immediate development assistance? Read on and make sure to stay secure!

Magento has a vibrant ecosystem and a huge community of people mostly willing to help. They often provide quality insights into their own findings by sharing the knowledge, in most cases for free. So, if you are a developer or a Magento store owner, you can find many useful tools out there to help you run your businesses. One such tool allows you to put your site through a quick security check and get some good pointers into how to solve some of the identified issues.

What’s up with security of Magento shops?

Any open source system, especially one of the leading eCommerce software platforms, is open to, among others, persons and organizations that don’t have only good intentions, to put it nicely. To put it accurately, there are a lot of hackers out there who are jumping at opportunities when certain security exploits are found. Some of them are even actively creating such opportunities for themselves.

What is Magento doing?

Magento is among rarely active open source eCommerce platforms when it comes to addressing any potential security exploits. They are releasing frequent security patches that should be applied by store owners or (in most cased) their development partners as soon as possible.

What are the most important patches?

Every security patch is important, however this all started back in 2015 with the infamous Shoplift Bug and SUPEE-5344 patch. This patch came out to help with the issue which saw thousands of websites vulnerable to potential store hijacking.

So, if this patch is not applied to your store, make sure to act swiftly. Additionally, the patches you should be on the lookout are: SUPEE-7405 and SUPEE-8788. There are many more, but these specifically can cause quite a lot of issues for you and your customers.

What can you do?

If you are on the merchant side of the equation (store owner, manager, anyone handling daily operations really), you can do a quick security check of your store by simply running your website through MageReport scan. If you are a developer, well – you can do the same 🙂

What is MageReport and what is it telling me?

Here’s a snippet from their own website:

This free service gives you a quick insight in the security status of your Magento shop(s) and how to fix possible vulnerabilities. MageReport.com is made by the Magento hosting specialists of Dutch provider Byte.

Essentially, the service is looking for proof/indication that you have official Magento patches installed. It also does security check over some other more or less known threats that can be mitigated rather easily.

IMPORTANT: If you are working with the latest versions of Magento 1 and/or Magento 2 – the latest stable releases being 1.9.3.3 (CE), 1.14.3.3 (EE), 2.1.7 (CE and EE) – chances are you are pretty safe, because in the process of upgrading your team have probably patched the site already. And there is also a chance that you can get false positives/negatives from this report in some cases. Still, it doesn’t hurt to check.

What to do next?

First off, if you’re seeing mostly red or orange in your scan results, you know you’re in a bad place. Depending on which patches your store are missing and your Magento version, the security of your store data may have been compromised.

Not good

Good

No need to panic right away, though. You should get in touch with your development team and ask them about this. As MageReport says, their report isn’t 100% accurate because they don’t have direct access to your store’s code. So, if you have a development team you trust, you should be in good hands. Simply ask them what’s the status about some of the missing patches (whether those are indeed missing). Then – work out a plan together to improve the security of your overall installation.

If you don’t have anyone actively working on or monitoring your store, you can get in touch with us directly to see how we can help.

If you’re getting mostly green results – well, hats off to you and your development team. You’re keeping the installation mostly safe and up to date with the latest security patches. Keep up the good work and don’t let new patches slide by you 🙂

Also, make sure to bookmark Magento Security center and keep your eye open on the incoming security news.

Stay safe!

The post How to run a quick security check of your Magento store? appeared first on Inchoo.

]]>
http://inchoo.net/magento/quick-security-check-magento-store/feed/ 2
Klevu – search engine that will increase revenue, self-learn and improve UX http://inchoo.net/magento/magento-search/working-with-klevu-search/ http://inchoo.net/magento/magento-search/working-with-klevu-search/#comments Wed, 28 Jun 2017 11:17:03 +0000 http://inchoo.net/?p=29830 One of most important parts of every store is its Search functionality. Implementing a more advanced search solution can have positive and big impact on search function, which leads to significant increase in conversion rate. After all, visitors trust in search results. If your store’s search experience is good, users will use it more than...

The post Klevu – search engine that will increase revenue, self-learn and improve UX appeared first on Inchoo.

]]>
One of most important parts of every store is its Search functionality. Implementing a more advanced search solution can have positive and big impact on search function, which leads to significant increase in conversion rate. After all, visitors trust in search results. If your store’s search experience is good, users will use it more than browsing by clicking through categories.

With artificial intelligence and self learning processes, search engines became even more powerful and provide us with features that were not possible before.

Klevu is one of those search engines – it is fast (really fast), it provides high level of customisation and most important – it learns from your customers and self-improves which results in more accurate search results and increase in revenue.

A few key takeaways from developer’s side:

Responsive works well out of the box, styling is decent and code is something you can work with.

It’s dynamic filters automatically create all relevant filters in the search results.

Error tolerance, as an enhanced keyword search index, ensure that shoppers are always connected to the right products.

Klevu works with both Magento 1 and Magento 2.

(for full feature list, check this link)

Installing Klevu

Installation is pretty much straightforward and same as with other extensions so i won’t talk much about it. After Extension has been installed, you must configure it and create your account.

Go to:

System > Configuration > Klevu > Search configuration

and start configuration wizard:

There are some prerequisites to be met, so check them out in case of any issues.

Klevu Dashboard

When you finish with Klevu wizard, you will be provided with credentials to access Klevu Dashboard where you can configure and edit Klevu to suit your needs.

There are many options over there so go through tabs and get yourself familiar with features.

In terms of styling, you will have to edit 2 areas of Klevu search – Search results dropdown and Search results page.

Under Customization section, you can change the look and feel of your Klevu search.

In Search as You Type Layout tab, you can define the look of search dropdown:

You can choose whether you will use Instant faceted search or Instant autocomplete layout first one displays filters on the left while second one is displaying suggestions and results under.

You can choose between grid or list display, as well as how many products you wish to feature. Pretty much everything you need for start.

Klevu Styling

Under Customize CSS tab, you can edit CSS used for styling of search results dropdown.



You can then save changes and preview the styling on your testing site. Sometimes, Klevu needs few minutes to update changes so be patient. 🙂

If you click on Search results page tab, you will see screenshots and explanation on how to change styling of search results page.

As you can see, it is not possible to change CSS in Klevu Dashboard (like we did for search suggestions), instead we are informed that we can use our own default Magento search results page (in which case we will have our theme styling and no additional CSS modifications will be needed) or we can use Klevu search results page. In that case, styling is done like with any other extension – editing files locally.

Same story as always – copy files from base to your package/theme and start with your magic.

Klevu data

By default, Klevu uses the following attributes in search: name, sku, description, short_description, price and rating.

You can add other custom attributes as well:

As you can see, you can tell Klevu which attribute values will be used in search. Klevu works great with receiving data and sending results back to user but if you want to display some custom product data on search results page, you may be in problem.

Read next…

Klevu Search results page

You can use two types for search results page – Klevu search result page or Magento search results page. Although decision seems pretty much straightforward, there are few differences that you must be aware before making that decision.

If you use:

  • Magento search results page
    – no need for additional styling (if search results page is already styled)
    – no need for additional structural changes
    – attributes – multiselect doesn’t work
    – ajaxed search results don’t work
  • Klevu search results page
    – instant, fast ajaxed results
    – multiselect Attributes
    – requires styling
    – custom product-related information not shown

Using Klevu search results page may cause an issues with custom data attached on your products. Although you can send additional data to Klevu and Klevu will provide results based on that data, it will not send that data back which means these informations will be missing on frontend. Reason for that is simple – Klevu is not aware of custom data shown on your category listing/search results page. You can define which custom attributes will be included in search but Klevu is not sending that data back, it is only using it to output search results.
That is a problem since that custom data can be really important for users.

Hopefully, there is a solution – Klevu support team. Of course, you can’t modify core functionalities by yourself so you will have to contact these guys. They will then do necessarily changes on their end or you will receive additional code to implement on your site.

I must say that we did this kind of work with klevu support and they did great job. They always react fast and get the job done.

Big question now is – Should we use default Magento search results page template or we should go with Klevu?
Basically, it all depends on project scope.

For heavier projects – it is better to use Magento search results page as in that case you will have your own structure and control over what is shown. On the other side, you will have to do Ajax functionality by yourself, as well as multiselect of attributes in layered navigation.
Using Klevu search results page means you will have to work with Klevu team and if there is a heavy customisation needed, whole process may take a while.

For clean, straightforward project with less custom information, go with Klevu search results page as search speed is just amazing and like i said, results are ajaxed and multiselect works. Results page styling will not follow Theme design so that will be extra work from your side.

Conclusion

Klevu is definitely something you should try/suggest to your clients. For us, it has proven to be one of best search engines we have ever tried.

Klevu is running on several of our projects and clients are satisfied with it.
And the best part – it will only get better since it learns from users and adjusts search results accordingly.

Klevu also offers 14 days of free trial which is enough time for first impressions and looking “under the hood”.

If you wish to know more, go on their site and check out integration guides or other documentation – everything is well written and explained.

We’ve implemented Klevu for our client BAUHAUS Croatia. This home and garden specialists have seen great results after achieving new development milestone, which also included implementation of Klevu search. 74% higher eCommerce Conversion rate, 105% increase in total transactions and 143% jump in Revenue!

If these numbers sound great, let’s see what we can achieve for your store!

The post Klevu – search engine that will increase revenue, self-learn and improve UX appeared first on Inchoo.

]]>
http://inchoo.net/magento/magento-search/working-with-klevu-search/feed/ 3
Version control systems in UI design http://inchoo.net/magento/design/version-control-systems-ui-design/ http://inchoo.net/magento/design/version-control-systems-ui-design/#comments Tue, 30 May 2017 10:58:43 +0000 http://inchoo.net/?p=29565 Our design team switched to working in Sketch a while ago but it wasn’t until version 43 we really started seeing some opportunities to change our workflows more significantly. It was also an opportunity for more designers to work on the same project as well as collaborate with developers more easily. The case of version...

The post Version control systems in UI design appeared first on Inchoo.

]]>
Our design team switched to working in Sketch a while ago but it wasn’t until version 43 we really started seeing some opportunities to change our workflows more significantly. It was also an opportunity for more designers to work on the same project as well as collaborate with developers more easily.

The case of version 43

With Sketch version 43, Bohemian Coding quietly announced – a revised file format. Behold. They introduced a new file format that allows reading and writing .sketch files manually or programmatically and go from Sketch straight to implementation. What does that mean for our workflow? Sketch had a file format that was workable not only from the design side but developer side too.

An unzipped Sketch file created .png preview and human-readable .json files. This created a question for us – could our design and development teams collaborate seamlessly? Are we finally to feed design assets directly to the development pipeline and integrate .sketch file at the same time as development?

By having similar repositories we could both edit the same designs.  That could be any element, modular component or an entire screen UI. Anything visual can now go back and forth between Sketch and UI frontend seamlessly – in theory. What does that mean for us? We’d have to apply distributed version control to support this new, high-level way of collaborating not only within our design team but with developers outside the team as well to create a safe and responsible environment for project development. We also needed to find a way of knowing which one of us made changes, when they were made and if we can roll back to them. Seeing visually what these changes would be an added bonus.

Just a side note, all of this also meant that documents created with Sketch 43 would not be compatible with previous versions. It was the quietest end of an era, perhaps for a reason.

Talking to our backend developer and testing this new file format we found that this new Sketch format offers a different workflow, but it wasn’t the smoothest one. We weren’t quite convinced in the widely spread “Design is code, code is design” line. Sure, this brings different disciplines closer, but it doesn’t equate them. At least not yet. The whole rename .sketch extension to .zip, unzip it and then connect to GitHub (which we use) process does work (and you get to see the changes in the preview .png). But, for starters, it would be a lot easier if there was some sort of Sketch launcher that would eliminate the extension renaming process.

It doesn’t seem possible to use branches in an effective way (they can’t be merged and all the integration work needs to be done manually) so we, designers can’t link our system with version control and as a consequence can’t have a good overview of the all the progress done on the design. Also, simply put, this is a great concept but we just don’t see a workflow where a developer might find himself changing design elements and checking that code back in so the Sketch app can render those changes next time we work on it.

Aaron Weyenberg (UX lead from TED) best described when our workflows would actually improve. “It’ll be from Sketch’s open format leading to more powerful plugins and third party tools to make the day to day collaboration of designers and developers faster while not upending their existing habits.”

Sharing is caring

Recently we had the issue of multiple designers working on the same project. There were constant debates as to which file is the “freshest” and manual overriding to create one master file. It moved us to explore versioning systems we could use within our design team. Libraries, as we’ve seen with Craft, works well if you’re willing to create separate text styles, color and uncategorised layers that can then be used through multiple .sketch files.

But, what if entire layouts and pages need to be changed and rearranged, not just styles and components. How do you update structure and flow?

Symbols work great for single-player, single-file mode, libraries are more multi-player, multi-file kind of situation, but what about multi-player, single-file scenario? At what point and how do we unify our work and keep a neat overview of it?

The tools of the trade

Developers already have tools like GitHub and Source Tree – we needed a similar version control system. Enter Folio, Plant and Abstract.

Folio

Folio is a Mac only app that allows you to join existing Git repositories. You can clone existing projects from anywhere, including Gitlab, Github, Bitbucket or your own Git server. Folio will automatically stay in sync. Unlike the other apps it has no Sketch plugin and is available for free in trial mode through a Free Viewer. It works with most files (Photoshop, Illustrator, Sketch, Antetype & SVG out of the box) and you can add support for most files that have Quicklook previews.

Folio makes it quite easy to add a Git repository and allows you to browse all files assets and work with them. When you update your Sketch file, Folio will automatically update (commit and push) it. It also keeps all versions of a file, so you can easily review them. However, it’s a bit messy since there is no folder organisation – just one library. Also, In Folio every commit you make is on a single file. Select a file on the left, show its version history on the right. Other than your written description, the only way to see which components on which artboards were changed is to compare them visually.

Plant

Plant is also a Mac only app with a Sketch plugin. It includes Cloud sync, version history, conflict resolution but seemingly you cannot use Plant with other version controlled softwares than the proprietary one (currently you get 1 GB). It’s based on pages (and has filtering options) – it recognises which artboards you made changes to and suggest them as changes ready to be sent. It has the ability to show only the modified artboards as well as comments to each version in a pretty neat preview mode of the artboard. It also syncs pretty fast. We only wish it was more clear as to what is the actual master file (since that’s our main goal). Right now, each version you change is listed in the history on the right and branching and merging is not as clear as we would like them to be.

Abstract

We have to give it up to Abstract as they seem to be our favourites. It took us no time to find our way around the app as it’s very clear what your commits and branches are as well as what the master file is. We had two questions for the crew that generously invited us to try out the Private Alpha – when can we buy Abstract and can we use our own GitHub (since that is what the company uses as a versioning tool) to clone projects and stay in sync. At the moment you can’t use Abstract with other version controlled softwares (i.e. Github and Bitbucket) than the proprietary one. We’re still waiting for a reply on if that would change. Currently it is available only for Mac and Sketch but it was announced that it would also be available for Adobe.

As they put it on their blog “Throw Abstract in the mix, and suddenly everyone on a team has access to a rich history of why certain things are the way they are, the decisions that led to the current state of the work, and a way to instantly start contributing in a meaningful way.” Can’t wait for the Public Beta!

 

I also have to give thanks where thanks is due – to the backend developer who helped create this article. Thanks for all the help and collaboration!

The post Version control systems in UI design appeared first on Inchoo.

]]>
http://inchoo.net/magento/design/version-control-systems-ui-design/feed/ 3
External database connection in Magento http://inchoo.net/magento/magento-database/external-database-connection-magento/ http://inchoo.net/magento/magento-database/external-database-connection-magento/#comments Thu, 27 Apr 2017 14:58:39 +0000 http://inchoo.net/?p=29408 Most of the time working with Magento, a single database connection is just enough. Magento has excellent system of adding new tables in database or extending existing ones. So, why would there be a need for an external database connection outside the Magento system? Well, one of the examples is data migration from another ecommerce...

The post External database connection in Magento appeared first on Inchoo.

]]>
Most of the time working with Magento, a single database connection is just enough. Magento has excellent system of adding new tables in database or extending existing ones. So, why would there be a need for an external database connection outside the Magento system? Well, one of the examples is data migration from another ecommerce system. In this article, a simple connection to external database is explained with CRUD (create, read, update, delete) examples.

Configuration

This external database connection is similarly defined as the Magento default one – in an XML configuration. The difference is that foreign connection is defined inside particular module’s XML configuration. It defines read and write adapters, setup and database credentials information. Foreign tables are defined in the same way as magento tables. They are under inchoo_foreignconnection_resource node so the model resource can be invoked later in the code. For demonstration purpose, there’s a frontend node in XML configuration that defines front name of the controller (fconn).

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_ForeignConnection>
            <version>1.4.2</version>
        </Inchoo_ForeignConnection>
    </modules>
    <global>
        <models>
            <inchoo_foreignconnection>
                <class>Inchoo_ForeignConnection_Model</class>
                <resourceModel>inchoo_foreignconnection_resource</resourceModel>
            </inchoo_foreignconnection>
            <inchoo_foreignconnection_resource>
                <class>Inchoo_ForeignConnection_Model_Resource</class>
                <entities>
                    <product>
                        <table>product_description</table>
                    </product>
                </entities>
            </inchoo_foreignconnection_resource>
        </models>
        <resources>
            <inchoo_foreignconnection_write>
                <connection>
                    <use>inchoo_foreignconnection_database</use>
                </connection>
            </inchoo_foreignconnection_write>
            <inchoo_foreignconnection_read>
                <connection>
                    <use>inchoo_foreignconnection_database</use>
                </connection>
            </inchoo_foreignconnection_read>
            <inchoo_foreignconnection_setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </inchoo_foreignconnection_setup>
            <inchoo_foreignconnection_database>
                <connection>
                    <host><![CDATA[localhost]]></host>
                    <username><![CDATA[username]]></username>
                    <password><![CDATA[password]]></password>
                    <dbname><![CDATA[db_name]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdo_type><![CDATA[]]></pdo_type>
                    <active>1</active>
                </connection>
            </inchoo_foreignconnection_database>
        </resources>
    </global>
    <frontend>
        <routers>
            <inchoo_foreignconnection>
                <use>standard</use>
                <args>
                    <module>Inchoo_ForeignConnection</module>
                    <frontName>fconn</frontName>
                </args>
            </inchoo_foreignconnection>
        </routers>
    </frontend>
</config>

Model

Next thing is a model that will use defined foreign connection to get data or to save data in a foreign database. Here, the model is initialized with the product table from XML configuration, that in this case defines product_description table.

class Inchoo_ForeignConnection_Model_Product extends Mage_Core_Model_Abstract
{
    protected $_eventPrefix = 'inchoo_foreignconnection_product';
    protected $_eventObject = 'product';
 
    protected function _construct()
    {
        $this->_init('inchoo_foreignconnection/product');
    }
}

Model resource class is also defined with the same xml configuration node in _init() function, but with the TABLE_PRIMARY_KEY parameter. In this class, several functions can be created that will work with external data.

First example is createDataInResource function, which inserts data in model’s table. It takes array of parameters that will be inserted.

Second example is a readDataFromResource function that fetches all data from model’s table. Read adapter must be defined first. It is a configuration node from xml that defines read connection. After read adapter definition, Magento database functions can be used (select(), from(), limit(), etc..). When query is constructed completely, it can be executed with read adapter. Data can be retrieved with fetchPairs() or fetchAll() function. fetchAll() is used to get all records returned from mysql.

updateDataInResource and deleteDataFromResource functions take additional $id parameter that defines which record will be updated or deleted.

class Inchoo_ForeignConnection_Model_Resource_Product extends Mage_Core_Model_Resource_Db_Abstract
{
    const TABLE_PRIMARY_KEY = 'product_id';
 
    protected function _construct()
    {
        $this->_init('inchoo_foreignconnection/product', self::TABLE_PRIMARY_KEY);
    }
 
    public function createDataInResource($values = array())
    {
        $writeAdapter = $this->_getWriteAdapter();
        try {
            $writeAdapter->insert(
                $this->getMainTable(),
                $values
            );
        } catch (Exception $e) {
            Mage::log('Unable to insert data to external resource. ' . $e->getMessage(), null, null, true);
        }
    }
 
    public function readDataFromResource()
    {
        $data = array();
        $readAdapter = $this->_getReadAdapter();
        $select = $readAdapter->select()
            ->from($this->getMainTable(), '*')
            ->limit(20);
 
        try {
            $data = $readAdapter->fetchAll($select);
        } catch (Exception $e) {
            Mage::log('Unable to fetch data from external resource. ' . $e->getMessage(), null, null, true);
        }
 
        return $data;
    }
 
    public function updateDataInResource($id, $values = array())
    {
        $writeAdapter = $this->_getWriteAdapter();
        try {
            $writeAdapter->update(
                $this->getMainTable(),
                $values,
                self::TABLE_PRIMARY_KEY . '=' . $id
            );
        } catch (Exception $e) {
            Mage::log('Unable to update data in external resource. ' . $e->getMessage(), null, null, true);
        }
    }
 
    public function deleteDataFromResource($id)
    {
        $writeAdapter = $this->_getWriteAdapter();
        try {
            $writeAdapter->delete(
                $this->getMainTable(),
                self::TABLE_PRIMARY_KEY . '=' . $id
            );
        } catch (Exception $e) {
            Mage::log('Unable to delete data from external resource. ' . $e->getMessage(), null, null, true);
        }
    }
}
class Inchoo_ForeignConnection_Model_Resource_Product_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract
{
    public function _construct()
    {
        $this->_init('inchoo_foreignconnection/product');
    }
}

Usage in controller

All these functions are demonstrated in IndexController class but since they are defined in model’s resource class, they can be called in any controller class.

class Inchoo_ForeignConnection_IndexController extends Mage_Core_Controller_Front_Action
{
    public function indexAction()
    {
        // Create
        $foreignProductCreate = Mage::getModel('inchoo_foreignconnection/product')->getResource();
        $foreignProductCreate->createDataInResource(
            array(
                'product_name' => 'Product name',
                'product_description' => 'Product description'
            )
        );
 
        // Read
        $foreignProductRead = Mage::getModel('inchoo_foreignconnection/product')->getResource();
        $result = $foreignProductRead->readDataFromResource();
        var_dump($result);
 
        // Update
        $foreignProductUpdate = Mage::getModel('inchoo_foreignconnection/product')->getResource();
        $foreignProductUpdate->updateDataInResource(
            3394,
            array(
                'product_name' => 'Product name updated',
                'product_description' => 'Product description updated'
            )
        );
 
        // Delete
        $foreignProductDelete = Mage::getModel('inchoo_foreignconnection/product')->getResource();
        $foreignProductDelete->deleteDataFromResource(3394);
    }
}

In most scenarios, Magento will use different type of external connection to retrieve or send data, but sometimes an external database connection like this will be the best way to go. One of the examples would be when you want to import products from another system to Magento with their xsell or upsell products. In that case, read connection would be used to retrieve product data and write connection would be used to save xsell or upsell product ids in a temporary table so they can be assigned to Magento product when all products from external system are imported. 

The post External database connection in Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/magento-database/external-database-connection-magento/feed/ 4
Experiences of running Magento 1 on PHP 7 http://inchoo.net/magento/experiences-running-magento-1-php-7/ http://inchoo.net/magento/experiences-running-magento-1-php-7/#comments Tue, 18 Apr 2017 11:29:50 +0000 http://inchoo.net/?p=29318 How time flies! It’s been almost a year and a half since we released Inchoo_PHP7 extension for Magento 1 (https://github.com/Inchoo/Inchoo_PHP7), and announced that in a blog post (http://inchoo.net/magento/its-alive/). We were just scratching our own itch – wanting to use all the performance benefits PHP 7 brought to run Magento faster. But, as the cover image...

The post Experiences of running Magento 1 on PHP 7 appeared first on Inchoo.

]]>
How time flies! It’s been almost a year and a half since we released Inchoo_PHP7 extension for Magento 1 (https://github.com/Inchoo/Inchoo_PHP7), and announced that in a blog post (http://inchoo.net/magento/its-alive/).

We were just scratching our own itch – wanting to use all the performance benefits PHP 7 brought to run Magento faster. But, as the cover image of that post inadvertently prophesied, we created a monster that escaped, and is continuing to terrorize the villagers to present day.

So, what happened in the meantime?

  • M1 is still going strong. M2 will take over eventually, but, in my humble personal opinion, that’s going be a very slow process.
  • On the other hand, PHP 7 is overtaking PHP 5 much quicker than previous versions used to replace their predecessors. (https://seld.be/notes/php-versions-stats-2016-2-edition). This is logical, because it really brings huge performance improvements, and it really is quite compatible, considering the major version number change.
  • Magento core became PHP 5.6 compatible in 1.9.3.x. Inchoo_PHP7 became PHP 7.1 compatible.
    • But, believe it or not, my humble personal opinion is that it’s better to run Magento on PHP 7.0 than 7.1 (https://github.com/Inchoo/Inchoo_PHP7/wiki/RunningOnPHP7.1).
      It’s really difficult to say, but I guess there are hundreds of M1 sites powered by Inchoo_PHP. Just Inchoo alone built from scratch or upgraded double figure of sites to PHP 7. Community seems to be going strong with it too, so I think I can say that it is quite tried and true by now.
  • With help from community, we found and fixed a lot of edge cases, and can say quite comfortably that we can make pretty much any M1 site work on PHP 7 without a problem.
    • And, in the last release, we even created a testing shell script, which can be very useful to find and fix potential problems.
    • Just keep in mind that this still is, and always will be, a developer-level extension. It can be “install and forget” if you are lucky, but, it would be good to have someone knowledgeable to set up, test everything and fix any issues. We had clients who came to us just for this, and we were always able to help them upgrade to PHP 7. And I don’t even want to say you need Inchoo to set it up. There are a number of developers and agencies in the Magento community that can help you out.

What’s ahead?

  • Looks like M1 core will continue it’s evolution. Going to Composer install, PHP 7, full page cache on CE, etc. – these are all things that clients are demanding and Magento experts are able to provide. Whether someone likes it or not, the whole ecosystem is large enough to be able to live and evolve on its own, directed by market pressures, and not someone’s will.
  • 3rd party extensions are the part of code we can’t fix with our extension. So, whether you are a 3rd party extension creator, a client, an integrator or anywhere else in community, please help spread the awareness that it’s good for everyone to be PHP 7 compatible.
  • PHP 7.2 will remove Mcrypt extension, and M1 core is quite dependant on it. There are a few workarounds we already tried out, but it’s not going to be pretty. For the time being, stick to PHP 7.0, and you won’t have problems.
  • Personally, I can’t wait for the moment when I’ll be able to use PHP 7 features when programming Magento, not just it’s performance. Stronger typing, for one, should bring less bugs and a more secure system. But that is still in far future from now, unfortunately.

 

TL;DR;

M1 runs (much) faster on PHP 7. Quite easy to set up due to a 3rd party module Inchoo_PHP7. MIT license. Great community support.

If you are having issues with performance of your Magento store, feel free to contact us to check technical state of your shop!

See you around!

The post Experiences of running Magento 1 on PHP 7 appeared first on Inchoo.

]]>
http://inchoo.net/magento/experiences-running-magento-1-php-7/feed/ 2
Making FedEx api show shipping estimate http://inchoo.net/magento/make-magento-fedex-api-show-shipping-estimate/ http://inchoo.net/magento/make-magento-fedex-api-show-shipping-estimate/#comments Mon, 10 Apr 2017 12:42:56 +0000 http://inchoo.net/?p=29265 There always comes the time when shopkeeper decides that he want’s to inform his customer of shipping estimate on checkout, so they could know approximately when they will get their goods. And for that, many shops today rely on API-s like ones from USPS or FedEx. Both of which are available for Magento. In this article...

The post Making FedEx api show shipping estimate appeared first on Inchoo.

]]>
There always comes the time when shopkeeper decides that he want’s to inform his customer of shipping estimate on checkout, so they could know approximately when they will get their goods. And for that, many shops today rely on API-s like ones from USPS or FedEx. Both of which are available for Magento.

In this article I will be showing you how to override FedEx carrier to return shipping estimate for given rates.

Overriding carrier

FedEx carrier works by sending request with given flags to FedEx server, who then, based on given flags, prepares response. After Magento receives response, it parses each rate as “Mage_Shipping_Model_Rate_Result” which he will later pass on to “Mage_Sales_Model_Quote_Address_Rate”. From which we will be able to access in template to show in frontend. For that, we will first override “Mage_Usa_Model_Shipping_Carrier_Fedex”.

Adding a flag for api

For FedEx API to know that it needs to return shipping estimate it needs to be given a “ReturnTransitAndCommit” flag. If we look into “Mage_Usa_Model_Shipping_Carrier_Fedex::_formRateRequest()” method, we will see that all it does is prepare flags into an array before it is sent to be parsed into request.

All we need to do here is rewrite original method and at the end of the array add our own flag.

Like in given example:

public function _formRateRequest($purpose)
{
   $ratesRequest = parent::_formRateRequest($purpose);
   $ratesRequest['ReturnTransitAndCommit'] = true ;//Here we are adding flag
   return $ratesRequest;
}

Storing the response data

After we receive data from api it will be parsed into stdClass,and in that class we are only interested in ‘RateReplyDetails’ array, which holds all our rates and their details, including their shipping estimates.

What we need to do here is pass our shipping estimate data into rate that will be passed on. For that we will be rewriting “Mage_Usa_Model_Shipping_Carrier_Fedex::_prepareRateResponse()” method.

protected function _prepareRateResponse($response)
{
   $costArr = array();
   $priceArr = array();
   // Array in which to store timestamp
   $deliveryTimeStamp = array();         //
   $errorTitle = 'Unable to retrieve tracking';
 
   if (is_object($response)) {
       if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') {
           if (is_array($response->Notifications)) {
               $notification = array_pop($response->Notifications);
               $errorTitle = (string)$notification->Message;
           } else {
               $errorTitle = (string)$response->Notifications->Message;
           }
       } elseif (isset($response->RateReplyDetails)) {
           $allowedMethods = explode(",", $this->getConfigData('allowed_methods'));
 
           if (is_array($response->RateReplyDetails)) {
               foreach ($response->RateReplyDetails as $rate) {
                   $serviceName = (string)$rate->ServiceType;
                   if (in_array($serviceName, $allowedMethods)) {
                       $amount = $this->_getRateAmountOriginBased($rate);
                       $costArr[$serviceName]  = $amount;
                       $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName);
                       //Store timestamp into prepared array
                       $deliveryTimeStamp[$serviceName] = $rate->DeliveryTimestamp; //
                   }
               }
               asort($priceArr);
           } else {
               $rate = $response->RateReplyDetails;
               $serviceName = (string)$rate->ServiceType;
               if (in_array($serviceName, $allowedMethods)) {
                   $amount = $this->_getRateAmountOriginBased($rate);
                   $costArr[$serviceName]  = $amount;
                   $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName);
               }
           }
       }
   }
 
   $result = Mage::getModel('shipping/rate_result');
   if (empty($priceArr)) {
       $error = Mage::getModel('shipping/rate_result_error');
       $error->setCarrier($this->_code);
       $error->setCarrierTitle($this->getConfigData('title'));
       $error->setErrorMessage($errorTitle);
       $error->setErrorMessage($this->getConfigData('specificerrmsg'));
       $result->append($error);
   } else {
       foreach ($priceArr as $method=>$price) {
           $rate = Mage::getModel('shipping/rate_result_method');
           $rate->setCarrier($this->_code);
           $rate->setCarrierTitle($this->getConfigData('title'));
           $rate->setMethod($method);
           $rate->setMethodTitle($this->getCode('method', $method));
           $rate->setCost($costArr[$method]);
           $rate->setPrice($price);
	   //Store timestamp into rate
           $rate->setDeliveryTimeStamp($deliveryTimeStamp[$method]); //
           $result->append($rate);
       }
   }
   return $result;
}

Almost done..

Before we start celebrating we will notice that we are still not getting that data in fronted. That is because we stored that data into “Mage_Shipping_Model_Rate_Result_Method” which is is not same type of rate as in template, where we have “Mage_Sales_Model_Quote_Address_Rate”. So we will additionally rewrite method “Mage_Sales_Quote_Address_Rate::importShippingRate()” and just pass our data whether its present or not.

public function importShippingRate(Mage_Shipping_Model_Rate_Result_Abstract $rate)
{
   parent::importShippingRate($rate);
   if ($rate instanceof Mage_Shipping_Model_Rate_Result_Method) {
       $this->setDeliveryTimeStamp($rate->getDeliveryTimeStamp());
   }
   return $this;
}

And we are done. Wherever you call for rates (in my case it was checkout/onepage/shipping_method/available.phtml – onepages shipping methods) fedEx api will provide shipping estimate.

Thanks for being with me and happy coding.

The post Making FedEx api show shipping estimate appeared first on Inchoo.

]]>
http://inchoo.net/magento/make-magento-fedex-api-show-shipping-estimate/feed/ 1
Wireframing a successful design for your online store http://inchoo.net/magento/design/wireframing-successful-design-online-store/ http://inchoo.net/magento/design/wireframing-successful-design-online-store/#respond Wed, 05 Apr 2017 10:33:18 +0000 http://inchoo.net/?p=29245 As designers, we’re often faced with a lot of questions about our process. We never just dive into design and bask in the glory of amazing typography and brilliant color schemes because without the phases that precede it – it just wouldn’t even begin to be possible. Beam me up, designer The worst feedback you...

The post Wireframing a successful design for your online store appeared first on Inchoo.

]]>
As designers, we’re often faced with a lot of questions about our process. We never just dive into design and bask in the glory of amazing typography and brilliant color schemes because without the phases that precede it – it just wouldn’t even begin to be possible.

Beam me up, designer

The worst feedback you can give to a designer is commenting how pretty the design is. We don’t aim for pretty, can’t learn from it and our clients aren’t always satisfied with just pretty. In the end, if our clients’ customers don’t see the use in the pretty that we’ve created – the webshop will fail at some point.

Our process includes mechanisms that minimise that risk and allow us to make informed decisions for a successful webshop. Before the actual design phase, we go through planning and wireframing.

At this point we answer some of the questions that even we can’t answer just by being designers. We have best practice knowledge and experience, but we need that custom touch. If we can take a sneak peek at your Google Analytics or Hotjar, we can answer everything from what resolutions we should be making our designs on (according to most visits on certain devices) to how descriptive we should make our labels (if the age group of your returning customers is a bit older).

Depending on if the project budget allows us, we’ll sit through Hotjar recordings to find out where the hiccups are on your current site and what user flows are actually not flowing so well. Our redesign or incremental changes need to perform better than that. The work becomes directed and concentrated like a tractor beam on a star ship. We want to pull your customers in, and do so pain free.

All work, some play

Along with all that analysing, we communicate with our clients – a lot.

We sit at our table a lot, drink coffee too much, headphones in ears – we look like we’re doing great in our own little world. The truth is there is always some communication going on. So. Much. Communication. Especially in the wireframing phase where a lot of relevant work gets done.

That’s great though – in some ways we, as designers, feel like the glue that keeps the project connected. We estimate the size of the project, provide input on what could work for the online store (through user flows and usability best practices) and we even get to draw it up. We get to experiment on features and be the ones to present it to the clients. Our wireframes, design layouts and quick prototypes are much more budget-friendly than actual implementation. All the while talking to frontend, backend, fellow design colleagues and of course – our clients.

There is a lot of dread amongst designers when it comes to client work and communication, but, we don’t think of this communication as trying to fight our client on every argument. Your client knows his business better than you, he knows the direction he wants to take it in – you need him just as much as he needs you, so, talk it out.

Say your client can’t visualise a wishlist flow or he doesn’t quite see how animating tiles on tablet will save vertical space and leave more room for products – wireframe it, prototype it, present it, resolve the issue.

                         

It’s not all black&white

This entire article went by without mentioning the latest design trends, without complaining how hard it is for us to get the exact design we have in our heads approved and how we’re missing proper tools to correspond to responsive demands.

The least of our problems is opening our tools and creating the design – it has its own challenges, but that’s on myself and my skills. It’s everything else – the planning, knowing the technology, the communication – that ends up being some of the hardest work and our best efforts in creating meaningful experiences and a successful online shop.

The post Wireframing a successful design for your online store appeared first on Inchoo.

]]>
http://inchoo.net/magento/design/wireframing-successful-design-online-store/feed/ 0
How I learned to stop worrying and love 3rd party modules for Magento http://inchoo.net/magento/learned-stop-worrying-love-3rd-party-modules-magento/ http://inchoo.net/magento/learned-stop-worrying-love-3rd-party-modules-magento/#comments Tue, 21 Mar 2017 11:34:50 +0000 http://inchoo.net/?p=29041 It’s just another day at the office. “Coffee tastes best on a Friday morning”, you think to yourself while reading emails from the previous day. Couple of guys are shouting in the back, and you’re shushing them angrily. With noise slowly diminishing, you find yourself checking an email with a request for custom change on...

The post How I learned to stop worrying and love 3rd party modules for Magento appeared first on Inchoo.

]]>
It’s just another day at the office. “Coffee tastes best on a Friday morning”, you think to yourself while reading emails from the previous day. Couple of guys are shouting in the back, and you’re shushing them angrily. With noise slowly diminishing, you find yourself checking an email with a request for custom change on one of your Magento stores. “Guys, we’ve got work to do. WeHeart3rdPartyModules Ltd. just requested a custom change on one of their stores. Who is willing to take on this one?”. Silence takes over the room. The only sound you hear is a fan from your colleagues PC (no need to ask why, Magento 2 is deploying static content). It seems like you’re in this alone. “Damn, I was hoping for a slow Friday”.

While PHPStorm is loading up, you’re reading the requirements:

Hi guys,

we would like to add a new column to our sales orders grid. We understand we have 20 of them already, but we really need this one – coupon description. Can you do this for us?

Thanks


“You mean rule description, not coupon, noob!”
, a smirk appears on your face. “I’ve done this a couple of times already. Also, I think we have an article written about this exact problem”.

Using your google-fu, an article pops up on the screen: How to extend Magento Order Grid? says the title. “Just what I need. This will be a piece of cake”.

IDE is finally loaded, and you’re rushing to get this done as fast as possible. “app, code, local, Inchoo” – your mind shouts – “you’ve done this a million times before” – but then you stop, and dark cloud just hangs over you. “Now I remember why there were no volunteers. This project has over a hundred 3rd party modules in local pool alone”. Not only your Friday is ruined, but the entire weekend does not look promising either.

“Well, let’s dig in, then.” Your train of though is something along the lines: “they have over 100 modules.. email mentions some earlier changes on sales order grid (having 20 columns already).. there must be a 3rd party module enabling them, or.. have we implemented this change already?”. You jump to Inchoo_WeHeart module’s config.xml and see no rewrites on 'sales_order_grid' block. It must be 3rd party then. Lovely.

Fortunately, your IDE is good in identifying class hierarchy, and in just a couple of clicks you find the class rewrite, and 3rd party module responsible (actually several of them, but two are disabled):  Amasty extended order grid. This should be fun. Module allows us to select columns to add to “Sales > Orders” grid. Adrenaline spikes up – “what if the column we need can be added via admin? That would be so great”. You check system.xml and adminhtml.xml, run to the admin and start looking for columns. “Of course it’s not possible, it was too good to be true”.

You’ve been in this situation before, and you do the usual routine: start randomly opening 3rd party module’s classes hoping to find something so obvious that would allow you to finish this as fast as possible. Of course, you fail. Looks like a more systematic approach is needed after all. You’re checking config.xml, opening up observers, helpers, and many other classes, tracing the code. Finally you find it. It’s so obvious now.

Now, off to coupon description field. You go through Magento database, and find that coupon description is located in 'salesrule_coupon' table, but there is no description. Description is in another table – 'salesrule'. Looks like we’ll need 2 SQL joins for this. “It’s just a few lines”, a voice in your head says, “no one will notice it. And it’s not a core code, right?”. After a short struggle, you resist the temptation and decide not to change the module’s code directly (sell your soul for a quick fix).

Copying the code to local pool (if the module is in community) is also an option, but we can, and should, do better than that. Is class rewrite good enough? In most situations, yes, and this is one of those situations too. You quickly whip up a rewrite: “I can do this in my sleep”, you think to yourself, but somehow manage to spend 45 minutes on getting it to work (damn XML typo).

<?php
 
class Inchoo_WeHeart_Helper_Ogrid_Columns extends Amasty_Ogrid_Helper_Columns
{
 
    function prepareOrderCollectionJoins(&$collection, $orderItemsColumns = array()){
 
        parent::prepareOrderCollectionJoins($collection, $orderItemsColumns);
 
        $collection->getSelect()->joinLeft(
            array(
                'salesrule_coupon' => $collection->getTable('salesrule/coupon')
            ),
            'order.coupon_code = salesrule_coupon.code',
            array('salesrule_coupon.rule_id as coupon_rule_id')
        )->joinLeft(
            array(
                'salesrule_rule' => $collection->getTable('salesrule/rule')
            ),
            'salesrule_rule.rule_id = salesrule_coupon.rule_id',
            array('salesrule_rule.description as coupon_description')
        );
 
    }
 
    function getDefaultFields(){
 
        if (!$this->_defaultField){
            $this->_defaultField = array_merge(parent::getDefaultFields(), array(
                'discount_description' => array(
                    'header' => Mage::helper('sales')->__('Discount Description'),
                    'index' => 'coupon_description',
                    'filter_index' => 'salesrule_rule.description'
                )
            ));
        }
 
        return $this->_defaultField;
    }
 
}

You look satisfied with the solution. Module has been changed, but in an unobtrusive way. Original logic has been kept, and new functionality added. With a few keystrokes, code is committed to a feature branch, and pushed to dev server. You test it there once more, and it looks like it’s working. Response message is sent:

“Hi WeHeart”, (no need to call them by their full name, they’re used to it), “changes have been made and pushed to dev server for you to test. Regards, Inchoo.

You close down the browser, and check the time: “Oh, looks like it’s time for my break”.

The post How I learned to stop worrying and love 3rd party modules for Magento appeared first on Inchoo.

]]>
http://inchoo.net/magento/learned-stop-worrying-love-3rd-party-modules-magento/feed/ 2
Using PostCSS with Sass http://inchoo.net/magento/magento-frontend/using-postcss-sass/ http://inchoo.net/magento/magento-frontend/using-postcss-sass/#comments Tue, 07 Mar 2017 07:37:01 +0000 http://inchoo.net/?p=28940 In this article we’ll be looking to a basic overview of PostCSS from the perspective of a developer whose current CSS development process includes use of CSS preprocessor, or in particular, Sass. If you’re a Sass user, there are a couple of approaches when starting out with PostCSS. You could make a complete switch and...

The post Using PostCSS with Sass appeared first on Inchoo.

]]>
In this article we’ll be looking to a basic overview of PostCSS from the perspective of a developer whose current CSS development process includes use of CSS preprocessor, or in particular, Sass. If you’re a Sass user, there are a couple of approaches when starting out with PostCSS. You could make a complete switch and recreate your basic preprocessing environment with PostCSS, or you could start using it as a supplement.

Many of you will say that you still only rely on your favorite preprocessor, but then, it’s possible that you’re also using Autoprefixer for vendor prefixing, and guess what? In this case, you have already included PostCSS into your workflow.

What exactly are we talking about?

PostCSS is a tool, or basically, just an API which handles its plugins written in JavaScript.

Comparing to Sass, which has a bunch of features out of the box, PostCSS comes as a blank plate, ready to be filled with the ingredients you need.

Basic Setup

Including PostCSS into your project is not a complicated process, especially if you have a basic experience of using some of the task runners, such as Gulp or Grunt.

As a simple example, let’s take a look at the following gulpfile.js.

var gulp = require('gulp'),
    postcss = require('gulp-postcss'),
    autoprefixer = require('autoprefixer');
 
gulp.task('css', function() {
  return gulp.src('src/style.css')
    .pipe(postcss(
      autoprefixer()
    ))
    .pipe(gulp.dest('dest/style.css'));
});

What we see here is a two step process:

  1. First, we include the main PostCSS module.
  2. Next, we add PostCSS plugin(s) we want to use (which in these short example is the only one – Autoprefixer).

Of course, like with any new gulp plugin which you include into your gulpfile.js, PostCSS module and any additional PostCSS plugin need to be installed first. This can be done in a terminal, with a simple command, familiar to all Gulp users:

npm install gulp-postcss autoprefixer --save-dev

Choosing plugins

So, which plugins do we need? Well, this comes to your individual choice. For an easy start or just for supplementing your preprocessing workflow with some additional power, you will certainly gain an instant benefit with these two:

  • Autoprefixer – probably the most popular PostCSS plugin, used for adding required vendor prefixes. As already mentioned at the beginning, there is high chance that you’re already using this one.
  • .box {
      display: flex;
    }
     
    // Result after processing
    .box {
      display: -webkit-box;
      display: -webkit-flex;
      display: -ms-flexbox;
      display: flex;
    }

  • Stylelint – a linting plugin useful for maintaining consistent conventions and avoiding errors in your stylesheets.

If you want to get in more deeper and recreate your basic Sass environment, most likely you’ll also need to require the following plugins:

$blue: #056ef0;
$column: 200px;
 
.menu_link {
    background: $blue;
    width: $column;
}
 
// Result after processing
.menu_link {
    background: #056ef0;
    width: 200px;
}
  • Postcss-nested – gives us a functionality of unwrapping nested rules like how Sass does it.
.phone {
    &_title {
        width: 500px;
        @media (max-width: 500px) {
            width: auto;
        }
    }
}
 
// Result after processing
.phone_title {
    width: 500px;
}
@media (max-width: 500px) {
    .phone_title {
        width: auto;
    }
}
@define-mixin icon $network, $color: blue {
    .icon.is-$(network) {
        color: $color;
        @mixin-content;
    }
    .icon.is-$(network):hover {
        color: white;
        background: $color;
    }
}
 
@mixin icon twitter {
    background: url(twt.png);
}
@mixin icon youtube, red {
    background: url(youtube.png);
}
 
// Result after processing
.icon.is-twitter {
    color: blue;
    background: url(twt.png);
}
.icon.is-twitter:hover {
    color: white;
    background: blue;
}
.icon.is-youtube {
    color: red;
    background: url(youtube.png);
}
.icon.is-youtube:hover {
    color: white;
    background: red;
}

One of the most interesting plugins that we’re mentioning last, is CSSNext. This is actually a collection of plugins that, together, give us a possibility to use the latest CSS syntax today. It transforms new CSS specs into more compatible CSS without a need to waiting for browser support. CSSNext has a lot of features and some of them are:

  • custom properties set & @apply
  • custom properties & var()
  • custom selectors
  • color() function
  • :any-link pseudo-class, etc.

In your CSS file you can do something like this:

// Example for custom properties set & @apply
:root {
  --danger-theme: {
    color: white;
    background-color: red;
  };
}
 
.danger {
  @apply --danger-theme;
}

Why should you use PostCSS?

So, if you already have an effective workflow and you’re satisfied with using your favorite preprocessor for some time now, you might be still asking yourself why do I need to learn another tool (or make the switch from Sass)? What are the benefits?

To answer these questions, let’s summarize some of the advantages:

  • Speed – even though in the meantime Sass got a significantly faster (e.g., LibSass), PostCSS is still the winner here
  • Modularity – reduces bloat; you only include the functionality that you need
  • Lightweight – with previous benefit, you get also this one
  • Immediate implementation – if you want a new functionality, you don’t have to wait for Sass to be updated; you can make it on your own

Of course, everything’s not ideal and there are also certain drawbacks:

  • Increased complexity – more planning is required (e.g., plugins must be called in a specific order)
  • A different syntax (compared to Sass)
  • PostCSS processing requires valid CSS

What’s next

It’s perfectly clear that PostCSS is all about the plugins. At the time of writing, there are more than 200 plugins available (and this number is only getting bigger). So, to go beyond the basics, you’ll need to search for other plugins that will extend this barebones setup.

Of course, if you find out that some handy functionality is missing, go ahead and solve the problem by making your own PostCSS plugin.

The post Using PostCSS with Sass appeared first on Inchoo.

]]>
http://inchoo.net/magento/magento-frontend/using-postcss-sass/feed/ 1
Session storage and influence on performance in large PHP applications http://inchoo.net/magento/programming-magento/session-storage-php/ http://inchoo.net/magento/programming-magento/session-storage-php/#comments Tue, 07 Feb 2017 12:32:30 +0000 http://inchoo.net/?p=28686 Session is something that PHP developers use in their everyday work. But how many of you did took some time to actually consider where are they stored and how does that impact your application? Should you even care? Does number of sessions influence performance of your application? Setting up the test for file-based sessions To test this,...

The post Session storage and influence on performance in large PHP applications appeared first on Inchoo.

]]>
Session is something that PHP developers use in their everyday work. But how many of you did took some time to actually consider where are they stored and how does that impact your application? Should you even care? Does number of sessions influence performance of your application?

Setting up the test for file-based sessions

To test this, lets create some code. Keep in mind that same apply to larger applications such us Magento as well, but for the purposes of this article, we will keep it simple:

<?php
 
// Start profiling
$start = microtime(true);
 
// Set header
header('Content-type: text/plain');
 
// Adjust session config
ini_set('session.save_path', __DIR__ . '/sessions/');
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 10);
ini_set('session.gc_maxlifetime', 3000);
 
// Init session
session_start();
 
// End profiling
$duration = (microtime(true) - $start);
 
// Output the data
echo number_format($duration, 12) . PHP_EOL;

As you can see from the code above, the script doesn’t actually do anything. It allows you to set session configuration (to allow easier testing) while the only operational code here is – starting a session.

Before we go any further, let’s revise what those session configuration settings actually mean (though, you should already know this):

  • save_path – This is self-explanatory. It is used to set location of session files. In this case this is on file system, but as you will se later, this is not always the case.
  • gc_probability – Together with gc_divisor setting, this config is used to determine probability of garbage collector being executed.
  • gc_divisor – Again, used to determine probability of garbage collector being executed. This is actually calculated as gc_probability/gc_divisor.
  • gc_maxlifetime – Determines how long should session be preserved. It doesn’t mean that it will be deleted after that time, just that it will be there for at least that long. Actuall lifetime depends on when the garbage collector will be triggered.

Results of file-based session storage

To complete the test, I have executed 1000 curl requests to my server and stored the result returned by our ‘profiler’. With that data, I have created histogram of the response time:

As you can see, nothing really happens, application is working well in all cases. On the other hand, these are only 1000 sessions on extremely simple script, so let’s generate better environment. For the purpose of the test, I have used 14 days. For the number of sessions per day, I have opened random Google Analytics account of one of my Magento project, and determined that we will use 40’000 session per day. Roughly saying, we need 550’000 sessions to begin testing, which were generated by executing same amount of CURL requests to the server.

So we are ready to check what happens. I have executed another 1000 curl requests, again logging the response and generating the histogram of results:

session stored in file with GB turned on

As you can see, the results are very different. We know have two kind of results – ones that are executed properly, and ones that have severe performance penalty (700% longer execution time than usual). So what happened here? Well, simply said – garbage collector was triggered.

Explaining the results

If you recall from the beginning of the post (or take a look at the source), we defined some probability of GC being triggered. This means that given the calculate probability, some requests (to say at random) will trigger GC. This doesn’t mean that actual sessions will be deleted, but all of them will be checked, and delete if required. And that exactly is the problem.

Looking back at the script, we have defined the probability of GC being triggered as 1/10, and that is exactly what is observed on image above – 900 out of 1000 executed properly, while the rest 100 which executed GC took significantly longer to process. To confirm our findings, we will adjust gc_probability to 0 and thus disable GC. Repeating the same test, we observe following:

session stored in file with GB turned off

Now, we have only one data set, and that is the first group from the graph before. Difference in execution time is minimal, and application runs evenly across all requests. One may say that this s the solution to the problem, but keep in mind that currently nothing is deleting those session from our storage.

And last thing to note here, that in the example above with th GC turned on, due to my settings, none of the session files were actually deleted. When I did trigger deletion of the files, it took about 27 seconds to clear 90% of the files. If this is going to occur on the production server, you would have nasty problems during those 27 seconds.

Setting up the test for redis-based sessions

Next, I have tried what happens if you put those sessions into Redis server, instead of keeping them in files. For that, we need, among other things, altered version of our script:

<?php
 
// Start profiling
$start = microtime(true);
 
// Set header
header('Content-type: text/plain');
 
// Adjust session config
ini_set('session.save_handler', 'redis');
ini_set('session.save_path',    'tcp://127.0.0.1:6379');
ini_set('session.gc_probability', 0);
ini_set('session.gc_divisor', 10);
ini_set('session.gc_maxlifetime', 30000);
 
// Init session
session_start();
 
// End profiling
$duration = (microtime(true) - $start);
 
// Output the data
echo number_format($duration, 12) . PHP_EOL;

There is a very small difference in code, we only told PHP to used redis-php module to store sessions into Redis server specified by IP. Since I know nothing will happen with lower number of sessions in storage, I went and regenerated those 550’000 session before executing any tests.

Results of redis-based session storage

With everything read, we can execute the tests, the same way we previously did:

Once completed, I have adjusted gb_probabilty gain, and repeated the test:

Explaining the results

Unlike the previous test with file-based sessions, there is not really a difference in performance here. And this is basically because Redis internally can deal with session lifetime, which means that PHP does not have to. In other words, GC settings related to session no longer have influence on application performance.

Conclusion

Looking back at two examples, you can clearly see the difference between two storage types. Even though file storage is somewhat faster for majority of requests, it becomes an issue with large number of files. Even though only smaller portion of session files is actually deleted, when GC is triggered, all files will be checked. You can overcome this by disabling GC, but keep in mind that in such case, you must setup your own GC to serve the same purpose (cron process that relays on file system time-stamps). Of course, you can better time it, and it is enough to execute it once per day to lower stress, but it needs to exists.

On the other hand, you can use Redis, which is somewhat slower. How much slower, it depends on your setup. In my case, Redis was running on the same machine and we can observe performance penalty of 1ms in average. If the setup is different and Redis is running on remote server, you can expect heavy impact on the performance (for an example, in case of poor connection between servers).

My recommendation would be to use files with custom GC when application is running on single web server. If you have Multi-node setup, than Redis will be better option for you, but keep an extra eye on speed of the linke between your servers.

The post Session storage and influence on performance in large PHP applications appeared first on Inchoo.

]]>
http://inchoo.net/magento/programming-magento/session-storage-php/feed/ 1
Implementing javascript minifier http://inchoo.net/magento/programming-magento/implementing-javascript-minifier/ http://inchoo.net/magento/programming-magento/implementing-javascript-minifier/#comments Tue, 17 Jan 2017 13:08:47 +0000 http://inchoo.net/?p=27989 Implementing javascript minimization in Magento can help your page load time by compressing your javascript files and making them smaller for users to download. Along with CSS minimisation it can be a great asset for decreasing page loading time. In this article I will primary cover where minimization should be implemented, since writing a full code for...

The post Implementing javascript minifier appeared first on Inchoo.

]]>
Implementing javascript minimization in Magento can help your page load time by compressing your javascript files and making them smaller for users to download. Along with CSS minimisation it can be a great asset for decreasing page loading time.

In this article I will primary cover where minimization should be implemented, since writing a full code for dependable minifier is not a small task.

Although any custom php minimization code for javascript can be used, I will use slightly adjusted version of open source php minifier I tested and found really good written. Changes are done mainly to adapt it to Magento best practices, functionality remains the same. Open source PHP Javascript Minifier link.

First step in implementing this should be simple config switch in order to be able to turn it off for debugging purposes (place this is system.xml):

<?xml version="1.0"?>
<config>
    <sections>
        <dev>
            <groups>
                <js>
                    <fields>
                        <minimize_js translate="label">
                            <label>Minimize JavaScript Files</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                        </minimize_js>
                    </fields>
                </js>
            </groups>
        </dev>
    </sections>
</config>

Set the default value in config.xml by adding:

<default>
        <dev>
            <js>
                <minimize_js>0</minimize_js>
            </js>
        </dev>
</default>

Add declaration of core data helper rewrite also in your config.xml :

<helpers>
        <core>
             <rewrite>
                     <data>Inchoo_Minifier_Helper_Core_Data</data>
             </rewrite>
        </core>
</helpers>

Adjust the rewritten core helper code where Magento merges javascript, so we also minify it after merging (changes are marked between “INCHOO EDIT” in comments, and function minimizeJs is added, which serves as main handler of minifier code we’re about to add):

class Inchoo_Minifier_Helper_Core_Data extends Mage_Core_Helper_Data
{
    /**
     * INCHOO EDIT - added call for JS minimizing
     * @param array $srcFiles
     * @param bool $targetFile
     * @param bool $mustMerge
     * @param null $beforeMergeCallback
     * @param array $extensionsFilter
     * @return bool|string
     */
    public function mergeFiles(array $srcFiles, $targetFile = false, $mustMerge = false,
                               $beforeMergeCallback = null, $extensionsFilter = array())
    {
        try {
            // check whether merger is required
            $shouldMerge = $mustMerge || !$targetFile;
            if (!$shouldMerge) {
                if (!file_exists($targetFile)) {
                    $shouldMerge = true;
                } else {
                    $targetMtime = filemtime($targetFile);
                    foreach ($srcFiles as $file) {
                        if (!file_exists($file) || @filemtime($file) > $targetMtime) {
                            $shouldMerge = true;
                            break;
                        }
                    }
                }
            }
 
            // merge contents into the file
            if ($shouldMerge) {
                if ($targetFile && !is_writeable(dirname($targetFile))) {
                    // no translation intentionally
                    throw new Exception(sprintf('Path %s is not writeable.', dirname($targetFile)));
                }
 
                // filter by extensions
                if ($extensionsFilter) {
                    if (!is_array($extensionsFilter)) {
                        $extensionsFilter = array($extensionsFilter);
                    }
                    if (!empty($srcFiles)){
                        foreach ($srcFiles as $key => $file) {
                            $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
                            if (!in_array($fileExt, $extensionsFilter)) {
                                unset($srcFiles[$key]);
                            }
                        }
                    }
                }
                if (empty($srcFiles)) {
                    // no translation intentionally
                    throw new Exception('No files to compile.');
                }
 
                $data = '';
                foreach ($srcFiles as $file) {
                    if (!file_exists($file)) {
                        continue;
                    }
                    $contents = file_get_contents($file) . "\n";
                    if ($beforeMergeCallback && is_callable($beforeMergeCallback)) {
                        $contents = call_user_func($beforeMergeCallback, $file, $contents);
                    }
                    $data .= $contents;
                }
                if (!$data) {
                    // no translation intentionally
                    throw new Exception(sprintf("No content found in files:\n%s", implode("\n", $srcFiles)));
                }
                if ($targetFile) {
                    /** INCHOO EDIT START **/
                    if(isset($file)){
                        // Minimize only .js files
                        $fileExt = strtolower(pathinfo($file, PATHINFO_EXTENSION));
                        if(Mage::getStoreConfigFlag('dev/js/minimize_js') && $fileExt === 'js'){
                            $data = $this->minimizeJs($data);
                        }
                    }
                    /** INCHOO EDIT END **/
                    file_put_contents($targetFile, $data, LOCK_EX);
                } else {
                    return $data; // no need to write to file, just return data
                }
            }
 
            return true; // no need in merger or merged into file successfully
        } catch (Exception $e) {
            Mage::logException($e);
        }
        return false;
    }
 
    /**
     * INCHOO - main JS minimizer function
     * @param $data
     * @return bool|string
     */
    public function minimizeJs($data)
    {
        $minifer = Mage::helper('inchoo_minifier/minifier');
        $result = $minifer->minify($data);
 
        if($result !== false) {
            return $result;
        }
        return $data;
    }
}

Lastly, minifier code we will use to compress our merged javascript files:

<?php
/**
 * Copyright (c) 2009, Robert Hafner
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Stash Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Robert Hafner BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Magento port adjustment by Tomislav Nikčevski, 2016
 * JS Minifier
 * Usage - $this->minify($js);
 */
 
class Inchoo_Minifier_Helper_Minifier
{
    /**
     * The input javascript to be minified.
     *
     * @var string
     */
    protected $_input;
 
    /**
     * Output javascript buffer - this will be returned
     * @var
     */
    protected $_text;
 
    /**
     * The location of the character (in the input string) that is next to be
     * processed.
     *
     * @var int
     */
    protected $_currentPosition = 0;
 
    /**
     * The first of the characters currently being looked at.
     *
     * @var string
     */
    protected $_firstChar = '';
 
    /**
     * The next character being looked at (after firstChar);
     *
     * @var string
     */
    protected $_secondChar = '';
 
    /**
     * This character is only active when certain look ahead actions take place.
     *
     *  @var string
     */
    protected $_endChar;
 
    /**
     * Contains lock ids which are used to replace certain code patterns and
     * prevent them from being minified
     *
     * @var array
     */
    protected $locks = array();
 
    /**
     * Takes a string containing javascript and removes unneeded characters in
     * order to shrink the code without altering it's functionality.
     *
     * @param  string      $js      The raw javascript to be minified
     * @return bool|string
     */
    public function minify($js)
    {
        try {
            $js = $this->lock($js);
 
            $this->initialize($js);
            $this->loop();
            $js = $this->unlock(ltrim($this->_text));
            $this->clean();
 
            return $js;
 
        } catch (Exception $e) {
            $this->clean();
            Mage::log('Minifier failed. Error: ' . $e->getMessage());
            return false;
        }
    }
 
    /**
     *  Initializes internal variables, normalizes new lines,
     *
     * @param string $js      The raw javascript to be minified
     */
    protected function initialize($js)
    {
        $js = str_replace("\r\n", "\n", $js);
        $js = str_replace('/**/', '', $js);
        $this->_input = str_replace("\r", "\n", $js);
 
        // We add a newline to the end of the script to make it easier to deal
        // with comments at the bottom of the script- this prevents the unclosed
        // comment error that can otherwise occur.
        $this->_input .= PHP_EOL;
 
        // Populate "firstChar" with a new line, "secondChar" with the first character, before
        // entering the loop
        $this->_firstChar = "\n";
        $this->_secondChar = $this->getReal();
    }
 
    /**
     * The primary action occurs here. This function loops through the input string,
     * outputting anything that's relevant and discarding anything that is not.
     */
    protected function loop()
    {
        while ($this->_firstChar !== false && !is_null($this->_firstChar) && $this->_firstChar !== '') {
 
            switch ($this->_firstChar) {
                // new lines
                case "\n":
                    // if the next line is something that can't stand alone preserve the newline
                    if (strpos('(-+{[@', $this->_secondChar) !== false) {
                        $this->_text .= $this->_firstChar;
                        $this->saveString();
                        break;
                    }
 
                    // if secondChar is a space we skip the rest of the switch block and go down to the
                    // string/regex check below, resetting secondChar with getReal
                    if($this->_secondChar === ' ')
                        break;
 
                // otherwise we treat the newline like a space
 
                case ' ':
                    if(static::isAlphaNumeric($this->_secondChar))
                        $this->_text .= $this->_firstChar;
 
                    $this->saveString();
                    break;
 
                default:
                    switch ($this->_secondChar) {
                        case "\n":
                            if (strpos('}])+-"\'', $this->_firstChar) !== false) {
                                $this->_text .= $this->_firstChar;
                                $this->saveString();
                                break;
                            } else {
                                if (static::isAlphaNumeric($this->_firstChar)) {
                                    $this->_text .= $this->_firstChar;
                                    $this->saveString();
                                }
                            }
                            break;
 
                        case ' ':
                            if(!static::isAlphaNumeric($this->_firstChar))
                                break;
 
                        default:
                            // check for some regex that breaks stuff
                            if ($this->_firstChar === '/' && ($this->_secondChar === '\'' || $this->_secondChar === '"')) {
                                $this->saveRegex();
                                continue;
                            }
 
                            $this->_text .= $this->_firstChar;
                            $this->saveString();
                            break;
                    }
            }
 
            // do reg check of doom
            $this->_secondChar = $this->getReal();
 
            if(($this->_secondChar == '/' && strpos('(,=:[!&|?', $this->_firstChar) !== false))
                $this->saveRegex();
        }
    }
 
    /**
     * Resets attributes that do not need to be stored between requests so that
     * the next request is ready to go. Another reason for this is to make sure
     * the variables are cleared and are not taking up memory.
     */
    protected function clean()
    {
        unset($this->_input);
        $this->_currentPosition = 0;
        $this->_firstChar = $this->_secondChar = $this->_text = '';
        unset($this->_endChar);
    }
 
    /**
     * Returns the next string for processing based off of the current index.
     *
     * @return string
     */
    protected function getChar()
    {
        // Check to see if we had anything in the look ahead buffer and use that.
        if (isset($this->_endChar)) {
            $char = $this->_endChar;
            unset($this->_endChar);
 
            // Otherwise we start pulling from the input.
        } else {
            $char = substr($this->_input, $this->_currentPosition, 1);
 
            // If the next character doesn't exist return false.
            if (isset($char) && $char === false) {
                return false;
            }
 
            // Otherwise increment the pointer and use this char.
            $this->_currentPosition++;
        }
 
        // Normalize all whitespace except for the newline character into a
        // standard space.
        if($char !== "\n" && ord($char) < 32)
 
            return ' ';
 
        return $char;
    }
 
    /**
     * This function gets the next "real" character. It is essentially a wrapper
     * around the getChar function that skips comments. This has significant
     * performance benefits as the skipping is done using native functions (ie,
     * c code) rather than in script php.
     *
     *
     * @return string            Next 'real' character to be processed.
     */
    protected function getReal()
    {
        $startIndex = $this->_currentPosition;
        $char = $this->getChar();
 
        // Check to see if we're potentially in a comment
        if ($char !== '/') {
            return $char;
        }
 
        $this->_endChar = $this->getChar();
 
        if ($this->_endChar === '/') {
            return $this->processOneLineComments($startIndex);
 
        } elseif ($this->_endChar === '*') {
            return $this->processMultiLineComments($startIndex);
        }
 
        return $char;
    }
 
    /**
     * Removed one line comments, with the exception of some very specific types of
     * conditional comments.
     *
     * @param  int    $startIndex The index point where "getReal" function started
     * @return string
     */
    protected function processOneLineComments($startIndex)
    {
        $thirdCommentString = substr($this->_input, $this->_currentPosition, 1);
 
        // kill rest of line
        $this->getNext("\n");
 
        if ($thirdCommentString == '@') {
            $endPoint = $this->_currentPosition - $startIndex;
            unset($this->_endChar);
            $char = "\n" . substr($this->_input, $startIndex, $endPoint);
        } else {
            // first one is contents of $this->_endChar
            $this->getChar();
            $char = $this->getChar();
        }
 
        return $char;
    }
 
    /**
     * Skips multiline comments where appropriate, and includes them where needed.
     * Conditional comments and "license" style blocks are preserved.
     *
     * @param  int               $startIndex The index point where "getReal" function started
     * @return bool|string       False if there's no character
     * @throws \RuntimeException Unclosed comments will throw an error
     */
    protected function processMultiLineComments($startIndex)
    {
        $this->getChar(); // current endChar
        $thirdCommentString = $this->getChar();
 
        // kill everything up to the next */ if it's there
        if ($this->getNext('*/')) {
 
            $this->getChar(); // get *
            $this->getChar(); // get /
            $char = $this->getChar(); // get next real character
 
            // Now we reinsert conditional comments and YUI-style licensing comments
            if ($thirdCommentString === '!' || $thirdCommentString === '@') {
 
                // If conditional comments or flagged comments are not the first thing in the script
                // we need to append firstChar to text and fill it with a space before moving on.
                if ($startIndex > 0) {
                    $this->_text .= $this->_firstChar;
                    $this->_firstChar = " ";
 
                    // If the comment started on a new line we let it stay on the new line
                    if ($this->_input[($startIndex - 1)] === "\n") {
                        $this->_text .= "\n";
                    }
                }
 
                $endPoint = ($this->_currentPosition - 1) - $startIndex;
                $this->_text .= substr($this->_input, $startIndex, $endPoint);
 
                return $char;
            }
 
        } else {
            $char = false;
        }
 
        if($char === false)
            throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->_currentPosition - 2));
 
        // if we're here endChar is part of the comment and therefore tossed
        if(isset($this->_endChar))
            unset($this->_endChar);
 
        return $char;
    }
 
    /**
     * Pushes the index ahead to the next instance of the supplied string. If it
     * is found the first character of the string is returned and the index is set
     * to it's position.
     *
     * @param  string       $string
     * @return string|false Returns the first character of the string or false.
     */
    protected function getNext($string)
    {
        // Find the next occurrence of "string" after the current position.
        $pos = strpos($this->_input, $string, $this->_currentPosition);
 
        // If it's not there return false.
        if($pos === false)
 
            return false;
 
        // Adjust position of index to jump ahead to the asked for string
        $this->_currentPosition = $pos;
 
        // Return the first character of that string.
        return substr($this->_input, $this->_currentPosition, 1);
    }
 
    /**
     * When a javascript string is detected this function crawls for the end of
     * it and saves the whole string.
     *
     * @throws \RuntimeException Unclosed strings will throw an error
     */
    protected function saveString()
    {
        $startpos = $this->_currentPosition;
 
        // saveString is always called after a gets cleared, so we push secondChar into
        // that spot.
        $this->_firstChar = $this->_secondChar;
 
        // If this isn't a string we don't need to do anything.
        if ($this->_firstChar !== "'" && $this->_firstChar !== '"') {
            return;
        }
 
        // String type is the quote used, " or '
        $stringType = $this->_firstChar;
 
        // append out that starting quote
        $this->_text .= $this->_firstChar;
 
        // Loop until the string is done
        while (true) {
 
            // Grab the very next character and load it into firstChar
            $this->_firstChar = $this->getChar();
 
            switch ($this->_firstChar) {
 
                // If the string opener (single or double quote) is used
                // output it and break out of the while loop-
                // The string is finished!
                case $stringType:
                    break 2;
 
                // New lines in strings without line delimiters are bad- actual
                // new lines will be represented by the string \n and not the actual
                // character, so those will be treated just fine using the switch
                // block below.
                case "\n":
                    throw new \RuntimeException('Unclosed string at position: ' . $startpos );
                    break;
 
                // Escaped characters get picked up here. If it's an escaped new line it's not really needed
                case '\\':
 
                    // firstChar is a slash. We want to keep it, and the next character,
                    // unless it's a new line. New lines as actual strings will be
                    // preserved, but escaped new lines should be reduced.
                    $this->_secondChar = $this->getChar();
 
                    // If secondChar is a new line we discard firstChar and secondChar and restart the loop.
                    if ($this->_secondChar === "\n") {
                        break;
                    }
 
                    // append the escaped character and restart the loop.
                    $this->_text .= $this->_firstChar . $this->_secondChar;
                    break;
 
                // Since we're not dealing with any special cases we simply
                // output the character and continue our loop.
                default:
                    $this->_text .= $this->_firstChar;
            }
        }
    }
 
    /**
     * When a regular expression is detected this function crawls for the end of
     * it and saves the whole regex.
     *
     * @throws \RuntimeException Unclosed regex will throw an error
     */
    protected function saveRegex()
    {
        $this->_text .= $this->_firstChar . $this->_secondChar;
 
        while (($this->_firstChar = $this->getChar()) !== false) {
            if($this->_firstChar === '/')
                break;
 
            if ($this->_firstChar === '\\') {
                $this->_text .= $this->_firstChar;
                $this->_firstChar = $this->getChar();
            }
 
            if($this->_firstChar === "\n")
                throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->_currentPosition);
 
            $this->_text .= $this->_firstChar;
        }
        $this->_secondChar = $this->getReal();
    }
 
    /**
     * Checks to see if a character is alphanumeric.
     *
     * @param  string $char Just one character
     * @return bool
     */
    protected static function isAlphaNumeric($char)
    {
        return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
    }
 
    /**
     * Replace patterns in the given string and store the replacement
     *
     * @param  string $js The string to lock
     * @return bool
     */
    protected function lock($js)
    {
        /* lock things like <code>"asd" + ++x;</code> */
        $lock = '"LOCK---' . crc32(time()) . '"';
 
        $matches = array();
        preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
        if (empty($matches)) {
            return $js;
        }
 
        $this->locks[$lock] = $matches[2];
 
        $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
        /* -- */
 
        return $js;
    }
 
    /**
     * Replace "locks" with the original characters
     *
     * @param  string $js The string to unlock
     * @return bool
     */
    protected function unlock($js)
    {
        if (empty($this->locks)) {
            return $js;
        }
 
        foreach ($this->locks as $lock => $replacement) {
            $js = str_replace($lock, $replacement, $js);
        }
 
        return $js;
    }
 
}

That’s it, enable your minifier in admin under System->Configuration->Developer->JavaScript Settings->Minimize Javascript Files -> Yes.

Good luck, and don’t forget to clear cache.

In case you feel you need some extra help, we can offer you a detailed custom report based on our technical audit – feel free to get in touch and see what we can do for you!

The post Implementing javascript minifier appeared first on Inchoo.

]]>
http://inchoo.net/magento/programming-magento/implementing-javascript-minifier/feed/ 5