Autoformatarea codului

Relativ recent am descoperit că pot face auto-formatarea codului la… commit. Da, știu, mind blowing 😀

Eu am nevoie de formatarea SCSS, JS, JSON și PHP. Folosesc întotdeauna composer câteva pachete node, deci am packages.json și composer.json mereu în proiect.  Prin urmare, folosim câteva pachete Node pentru a face treaba:

$ npm i --save-dev cross-env husky lint-staged prettier prettier-stylelint stylelint-scss

Husky instalează hooks de git necesare, lint-staged execută hooks pentru fișierele staged, cross-env permite setarea de variabile. Apoi, adăugăm în package.json:

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },

  "lint-staged": {
    "*.{js,jsx}": [ "prettier --write", "git add" ],
    "*.{json}": [ "prettier --write --parser=json", "git add" ],
    "*.{scss,sass}": [ "prettier-stylelint --quiet --write", "git add" ],
    "*.{php,php_cs}": [ "php -l", "cross-env PHP_CS_FIXER_FUTURE_MODE=1 php-cs-fixer fix --config=.php_cs", "git add" ]
  }
}

Apoi avem fișierele de config:

// .prettierrc
{
	"useTabs": true,
	"semi": true,
	"singleQuote": true,
	"trailingComma": "all",
	"bracketSpacing": true,
	"jsxBracketSameLine": true,
	"arrowParens": "always"
}
// .stylelintrc
{
    "plugins": [
        "stylelint-scss",
    ],
    "rules": {
        "indentation": 2,
        "string-quotes": "single",
        "color-no-invalid-hex": true,
        "function-parentheses-space-inside": "always",
        "media-feature-parentheses-space-inside": "always",
        "selector-pseudo-class-parentheses-space-inside": "always",
        "max-empty-lines": 2,
        "scss/at-function-parentheses-space-before": "always"
    }
}

Pentru formatarea codului PHP avem nevoie să instalăm un pachet global:

$ composer global require friendsofphp/php-cs-fixer
<?php 

//.php_cs

return PhpCsFixer\Config::create()
  ->setRiskyAllowed(true)
  ->setRules(
	[
		'@Symfony' => true,
		'@Symfony:risky' => true,
		'@PHP71Migration' => true,
		'braces' => true,
		'array_indentation' => true,
		'array_syntax' => ['syntax' => 'short'],
		'dir_constant' => true,
		'heredoc_to_nowdoc' => true,
		'linebreak_after_opening_tag' => true,
		'modernize_types_casting' => true,
		'multiline_whitespace_before_semicolons' => true,
		'no_unreachable_default_argument_value' => true,
		'no_useless_else' => true,
		'no_useless_return' => true,
		'ordered_class_elements' => true,
		'ordered_imports' => true,
		'phpdoc_add_missing_param_annotation' => ['only_untyped' => false],
		'phpdoc_order' => true,
		'doctrine_annotation_braces' => true,
		'doctrine_annotation_indentation' => true,
		'doctrine_annotation_spaces' => true,
		'psr4' => true,
		'no_php4_constructor' => true,
		'no_short_echo_tag' => true,
		'semicolon_after_instruction' => true,
		'align_multiline_comment' => true,
		'doctrine_annotation_array_assignment' => true,
		'general_phpdoc_annotation_remove' => ['annotations' => ['author', 'package']],
		'list_syntax' => ['syntax' => 'short'],
		'phpdoc_types_order' => ['null_adjustment' => 'always_last'],
		'single_line_comment_style' => true,
	]
  )
  ->setCacheFile(__DIR__.'/.php_cs.cache')
  ->setIndent("\t")
  ->setFinder(
	PhpCsFixer\Finder::create()
	  ->in(__DIR__)
  );

Și cam asta e. De fiecare dată când se face commit, autoformatarea se aplică pe fișierul modificat. Asta înseamnă și un timp mai mare de commit, dar până acum nu am observat un delay mai mare de 5-10 secunde.

Evident, regulile sunt ajustabile, fiecare formatter având propriile opțiuni.

Learning React: Atributul key={}

Mă tot gândeam: cum ar fi să scriu câte un articol despre lucrurile de care m-am lovit învățând React? Prin urmare, ăsta este primul post. Dacă or mai veni altele sau nu… vedem.

Atunci când randezi un element într-un loop (map, forEach), este recomandat să îi adaugi acelui element un atribut numit key. Acesta nu va fi trimis în props copiilor și este folosit exclusiv de React pentru diverse chestii (gen update).

Greșeala nr. 1: index key

Prima greșeală făcută de mine a fost să folosesc index-ul din loop pentru key. Ceva de genul:

lista.map((el, index) => <div key={index} />)

Rezultatul? Când actualizam props… unele elemente randate rămâneau neschimbate. Câteva ore mai târziu m-am prins și de ce: cheile ar trebui să fie unice! Prin urmare, am purces să fac….

Greșeala nr. 2: random key

A doua greșeală a fost să presupun că un key random este în regulă:

lista.map((el, index) => <div key={index + Date.now()} />)

Chiar dacă merge, vor fi probleme de performanță. De ce? Pentru că schimbarea unui singur element din listă va duce la re-randarea tuturor elementelor. Ouch!

Sigur, la 10-20 elemente nu ar trebui să fie probleme, dar la 1-200?

Greșeala nr. 3: prefix

La un moment dat am făcut ceva greșit și am rămas cu impresia că aceste key trebuie să fie unice în aplicație. Lucru cât se poate de fals, ele ar trebui să fie unice în părinte.

Dar până să mă prind de chestia asta, am pus mereu un prefix. Aveam aplicația plină de key={`users-${user.id}`}. Nu neapărat rău, dar inutil de verbose.

Cum ar trebui sa fie de fapt?

Dacă citeam cu atenție documentația, aflam că, în cazul ideal, te folosești de id-ul fiecărui element. Pentru că ghici ce? Cel mai probabil elementele alea vin din DB unde … au un ID! Abia în cazul în care nu au un ID te folosești de index!

Bonus: map, forEach, reduce, filter

Indiferent dacă ai sau nu de gând să folosești React, îți recomand două lucruri:

  1. Migrează pe ES6. Sunt o grămadă de lucruri care îți fac viața mai ușoară: spread operator, destructuring, arrow functions, let/const, module etc. Chiar dacă adaugă un pas de compilare, merită efortul.
  2. Chiar dacă te încăpățânezi să nu o faci, încearcă să înțelegi cum se folosesc toate funcțiile de manipulare arrays. Spre deosebire de PHP, folosirea funcțiilor native îți va aduce un plus de performanță. În plus, arată și mai bine 🙂 Iată două exemple:
const invoiceNumbers = invoices.map(invoice => invoice.number)

// sau

var invoiceNumbers = [];
for (var i = 0; i < invoices.length; i++) {
  invoiceNumbers.push(invoices[i].number)
}
const invoicesGreaterThan = invoices.filter(invoice => invoice.value > 1000)

// sau

var invoicesGreaterThan = [];

for (var i = 0; i < invoices.length; i++) {
  if (invoices[i].value > 1000) {
    invoicesGreaterThan.push(invoices[i])
  }
}

Un mod simplu de a implementa „Open-Closed principle” în WordPress

OCP este unul dintre principiile SOLID despre care am mai scris și aici. Vreau să-ți arăt o modalitate simplă prin care poți implementa asta chiar dacă nu ești fan OOP: folosind filtre! Sigur, nu este foarte încapsulată toată povestea, dar cred că este un compromis care merită făcut în majoritatea cazurilor.

La proiectul la care lucrez zilele astea am o chestie care m-a făcut să mă gândesc la treaba asta. Am o listă de vreo 30-40 meta-fields, iar la câteva vreau să aplic câteva transformări, în funcție de tipul lor (e.g. dată, formatare text, galerie etc).

Prima soluție la care te gândești este, probabil, un switch/case:

switch( $key ) {
  case 'description':
    $value = wpautop($value);
    break;
  case 'date':
    $value = \DateTime::createFromFormat(/*...*/);
    break;
  case 'gallery':
    $value = formatMyGalleryField($content);
    break;
  // .....
}

Vezi deja unde ne îndreptăm: pentru fiecare caz în parte avem de adăugat o condiție. Lucrurile încep să se complice și mai mult când vrem să aplicăm două sau mai multe transformări sau când vrem o ordine diferită a transformărilor în funcție de $key.

Filtre!

Toată condiția de mai sus s-ar putea rescrie mai simplu

$value = apply_filters("ntz/transformations/key={$key}", $value, $key ... )

Unul din lucrurile care cred că sunt prea puțin folosite în lumea WP este posibilitatea de a folosi orice caractere în numele filtrelor, motiv pentru care vedem filtre ceva mai greu de citit (decât ar putea fi). De ce ai folosi underscore în loc de slash?

Simplu, nu?

Următorul pas este să adaugi toate filtrele de care ai nevoie:

add_filter('ntz/transformation/key=description', 'wpautop');
add_filter('ntz/transformation/key=date', 'myDateFormatter');
// ....

Vrei să adaugi mai multe transformări pentru același filtru? Nici o problemă!

add_filter('ntz/transformation/key=description', 'myDateFormatter');
add_filter('ntz/transformation/key=description', 'wpautop');
// ....

Ordinea nici măcar nu e foarte importantă, având în vedere că al treila parametru poate fi un integer ce reprezintă ordinea (sau prioritatea) de execuție.

img src

Using TGMPA with GitLab’s CI artifacts

TLDR: set some headers, see below.

TGMPA

TGMPA is a WordPress plugin that helps you to manage your theme dependencies. Just imagine your users experience: instead of ask them to check a README doc that recommends installing various plugins (and no way of enforcing them to do so), this panel will show up everywhere they go:

A basic config would look like this:


add_action('tgmpa_register', function () {
  // if the plugin is not hosted on WP, add * at the begining of the URL
  $plugins = [
    "simple-taxonomy-ordering" => "https://downloads.wordpress.org/plugin/simple-taxonomy-ordering.1.2.4.zip",
  ];

  $required_plugins = [];

  $config = [
    'is_automatic' => true,
    'dismissable' => false,
  ];

  foreach ($plugins as $plugin_name => $plugin_url) {
    $required_plugins[$plugin_name] = [
      'name' => $plugin_name,
      'slug' => $plugin_name,
      'required' => true,
      'force_activation' => true,
    ];

    if (substr($plugin_url, 0, 1) == '*') { // just a trickery to allow specifying a ZIP url
      $required_plugins[$plugin_name]['source'] = substr($plugin_url, 1);
    } else {
      $required_plugins[$plugin_name]['external_url'] = $plugin_url;
    }
  }

  tgmpa($required_plugins, $config);
});

GitLab CI

On almost evey project, I’m using composer for various PHP deps, nodejs for various JS deps and a few Grunt or Gulp tasks. Obviously enough, I don’t want to keep in my repo a huuuge vendor or node_modules folder, so I gitignore these suckers. But… this means that my code is NOT ready to use.

One of the thing I really, really like on GitLab is that I can have a build process run everytime I push some code. Why this is helpful? Well, GitLab CI deals with all these issues and I have the package ready to download from this address (this is called an artifact and is the result of the CI build process):

https://gitlab.com/iamntz/my-plugin-name/-/jobs/artifacts/master/download?job=my_plugin_name

Do you notice the job param? Is the same as the YAML key below!

However, what is the problem? You can’t download this package dirrectly; you must do this either from their UI or via an arcane curl command, that sets a http header:

curl -L --header "PRIVATE-TOKEN: XXYYZZ" "https://gitlab.com/iamntz/my-plugin-name/-/jobs/artifacts/master/download?job=my_plugin_name" -o "my-plugin-name.zip"

You can create a private token on this page. Just create one that only have read_registry scope enabled and set to not expire. Since this token can represent a security issue, please consider creating a dumb user to whom you provide only a read access and add that user to your project (instead of using your token).

.gitlab-ci.yml

image: tetraweb/php:7.1

before_script:
  - docker-php-ext-enable zip
  - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
  - php composer-setup.php
  - php -r "unlink('composer-setup.php');"
  - php composer.phar install --no-dev -o --prefer-dist # <<< add more tasks below
  - mkdir -p my-plugin-name
  - rsync -av --exclude-from='deploy-exclude-files.txt' . my-plugin-name # <<< this is where you'll have all the files you need to be deployed

my_plugin_name: # <<< this key is important and can be anything
  retry: 2
  cache: # <<< this block is optional, but it will improve build time 
  untracked: true 
  key: ${CI_BUILD_REF_NAME} 
  paths: 
    - node_modules/ 
    - vendor/ 
  artifacts: 
    name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}" 
    untracked: false 
    paths: - my-plugin-name/ 
  script: - echo 'Deploying!' 

In case you wonder, deploy-exclude-files.txt is a file that contains all files and folder you want to be excluded from your final ZIP file. It has one file/folder name per line and that’s all

Once the build process is done, as long as you are authentificated to gitlab, you will be able to access it on `https://gitlab.com/iamntz/my-plugin-name/-/jobs/artifacts/master/download?job=my_plugin_name` url.


Best of both worlds

But a problem arise: how can you specify that build artifact URL in your tgmpa config? Well, that’s easier than you’d expect: just set a header! If you’re digging into TGMPA code, you’ll notice that it uses WP_Http class to download external fiels. This means that… you can set some options!

add_filter('http_request_args', function ($r, $url) { 
  if (preg_match('/gitlab.com\/iamntz/', $url)) { 
    $r['headers']['PRIVATE-TOKEN'] = 'XXYYZZ'; 
  } 
  return $r; 
}, 10, 2); 

After that, you modify your tgmpa config to look like this (notice the * before the URL!):

$plugins = [ 
  "simple-taxonomy-ordering" => "https://downloads.wordpress.org/plugin/simple-taxonomy-ordering.1.2.4.zip",
  "my_plugin_name" => "*https://gitlab.com/iamntz/my-plugin-name/-/jobs/artifacts/master/download?job=my_plugin_name"
];

Yup, is that simple!

However, you may want to pay some attention to the preg_match('/gitlab.com\/iamntz/', $url) pattern and be as specific as you can (e.g. it may conflicts with GitLab’s releases feature). Just play around a little. 😉

Customizing Elementor’s default widgets

If you’re using Elementor for client work, probably you’ll hit some limits soon enough. Like, for example, you can’t customize buttons aspects on all widgets. Although is never documented, it is possible and I’ll show you a way of doing this. The example? Call to action widget (available only on Elementor Pro).

By default, you can only customize some basic stuff and if you want to reuse same style again, you’re kind of screwed.

This is a bit of nonsense, because Elementor already has a button component that you can use it outside of this widget! Why can’t I just use it here?

So, in order to make it work, we will need a couple of things. Elementor uses two ways of rendering content: one is JS based and is used only when you edit the page, the other one is PHP based and is used… everywhere else. Continuă să citești Customizing Elementor’s default widgets

Pre-popularea meniurilor în WordPress

De multe ori se întâmplă să fie nevoie să am meniurile gata pregătite în WP. Să fac un fel de seed la DB. Cum procedez?

În primul rând, facem un array cu elementele de meniu. Majoritatea sunt doar pagini, dar sunt situații în care vrem ceva mai… custom. De exemplu, vrem să adăugăm o anumită clasă pentru un element al meniului:

$menus = [
  'primary' => ['About', 'Stories', 'Community', 'Resources', 'Pet Supplies', 'Subscribe',
    [
      'menu-item-title' => 'Pet Portal',
      'menu-item-classes' => 'button button-hollow',
    ],

    [
      'menu-item-title' => 'Donate',
      'menu-item-classes' => 'button',
    ],
  ],
  'secondary1' => ['Adoption', 'Pet Care', 'Training', 'Youth Programs'],
  'secondary2' => ['Give', 'Take Action', 'Outreach', 'Sponsor'],
  'footer' => ['Contact', 'Careers', 'Articles', 'Events', 'Results'],
];

Cheile array-ului coincid cu meniurile înregistrate deja (să zicem în functions.php): Continuă să citești Pre-popularea meniurilor în WordPress

Principii SOLID în practică

Am fost întrebat mai deunăzi dacă pot îmbunătăți codul de mai jos:

class Tooltip
{
  protected $dataIsProcessed = false;

  public function __construct(array $data) {
    $this->data = $data;  
  }

  protected function processData() {
    if($this->dataIsProcessed) {
      return;
    }

    $this->dataIsProcessed = true;

    $this->data = array_merge([
      'title' => '',
      'body' => '',
      'meta' => ''
    ], $this->data);

    $this->data['body'] = stripslashes($this->data['body']);
    $this->data['title'] = do_some_stuff_with_the_title($this->data['title']);
    $this->data['meta'] = do_some_other_stuff_with_meta($this->data['meta']);
  }

  protected function getAttribute($attribute) {
    $this->processData();
    return $this->data[$attribute] ?? '';
  }

  public function style1() {
    return sprintf('<h2>%s</h2>',
      $this->getAttribute('title')
    );
  }

  public function style2() {
    printf('<h2>%s</h2><p>%s</p>',
      $this->getAttribute('body')
    );
  }

  public function style3() {
    return sprintf('<p>%s</p><div>%s</div>',
      $this->getAttribute('body'),
      $this->getAttribute('meta')
    );
  }
}


$tooltip = new Tooltip([
  'title' => 'Hello World!',
  'body' => 'Nice to meet you.',
  'meta' => 'Last update: last year'
])

echo $tooltip->style1();
echo $tooltip->style2();
echo $tooltip->style3();

Continuă să citești Principii SOLID în practică

WordPress: Încărcarea dinamică a articolelor cu un anumit term

Zilele trecute am avut următoarea situație:

  • În backend: o taxonomie custom cu câteva zeci de terms
  • În frontend: afișez ultimele zece articole dintr-un term + buton de încărcare a următorului term.

Prima idee a fost: încarc toate articolele dintr-un foc și fac toggle la vizibilitate cu JS. Dar se ajunge lejer la câteva sute de articole, lucru ce afectează performanța din toate punctele de vedere. Continuă să citești WordPress: Încărcarea dinamică a articolelor cu un anumit term

Coding tips: Interfețe

Un concept cu care am avut ceva de furcă la început a fost folosirea interfețelor. Peste tot găseam o explicație vag sumară, eventual și ceva legat de un contract și rămâneam cel puțin la fel de confuz ca înainte.

Din punctul meu de vedere, dacă nu plănuiești să ai un cod extensibil (e.g. prin plugin-uri), interfețele sunt overkill. Nu strică, dar nici nu aduc valoare.

Conceptele SOLID sunt another can of worms, despre care probabil voi scrie cu altă ocazie.

Pentru a înțelege scopul interfețelor, este util să înțelegi conceptele SOLID, în special open-closed principle, care zice că un obiect ar trebui să permită extinderea, nu modificarea. Continuă să citești Coding tips: Interfețe

De la Windows la Ubuntu si inapoi

De când mă știu a sta în fața unui computer am folosit Windows. Când am decis că vreau să devin programator am făcut trecerea către sisteme Linux. Mi-aș fi dorit un Mac pe vremea aceea dar cum nu îmi permiteam așa ceva am ales soluția cea mai apropriată. Se auzeau zvonuri despre Windows 9 când am început să folosesc ElementaryOs.

Mă așteptam să trec printr-o perioadă mai lungă de acomodare. Aveam impresia că voi fi nevoit să folosesc terminalul și că îmi va fi greu, că îmi va lipsi interfața grafică a Windows-ului. Dar am descoperit repede că terminanul nu înlocuiește componenta grafică a sistemului de operare ci o completează.

Pe atunci foloseam dual boot, lucram des cu Adobe Illustrator și After Effects și nu mă descurcam cu alternativele oferite pe Linux. Programele oferite de Adobe erau singurele care mă țineau captiv în lumea Windows.

Dar am început să lucrez cu RaspberryPi așa că am făcut alegerea logică de a folosi mai des Ubuntu și nu am fost dezamăgit deloc de alegere:

  • arată și se mișcă bine, consumă puține resurse;
  • ușor de costumizat și multe skin-uri disponibile;
  • terminal-ul este foarte util, competent și oferă un workflow mult mai bun pentru dezvoltare;
  • Inkscape ca alternativă bună pentru Illustrator;
  • Blender ca alternativă bună pentru After Effects;
  • flexibilitatea sistemului de operare mi-a permis dezvoltarea unor periferice proprii;
  • este gratuit!;

Nu îmi lipsea nimic din mediul Windows. Am înlocuit și suita Adobe cu programe open-source iar de gaming nu se pune problema (nu sunt gamer, dar la nevoie aveam Minecraft și Steam).

Toate beneficiile și costuri 0. Așa am renunțat la Windows și pentru calculatorul de acasă și pentru cel de la muncă.

Doar că mi-am schimbat jobul, și am primit un laptop cu Windows 10. Ce să zic? „Bine te-am găsit, ce știi să faci?”. Observ că îmi lipsește UX-ul din Ubuntu, Windows face niște chestii ciudate:

  • alege să iși facă update în cele mai proaste momente;
  • nimic nu se aproprie de terminal-ul linux (cmd este doar o fosilă, power shell e ciudat). Alternativele sunt mai bune ,Git Bash de exemplu, dar nici ele nu vin cu o experiență bună out of the box, tot trebuiesc configurate;
  • uneori ferestre noi apar cu bara de titlu (partea cu – [] x) în afara suprafeței de lucru. Nu am cum să mut fereastra așa că trag de unul din colțurile de jos ca ele să pice bine în desktop;
  • dacă m-am deconectat de la un monitor extern multe ferestre se deschid în acel spațiu ce acum îmi este inaccesibil. Chiar dacă resetez modul de proiectie (win+p) sau dau restart, ele tot acolo se deschid;
  • pentru Netflix folosesc un monitor extern cu niste boxe atașate de acesta. Cu Windows se întampla de multe ori să nu am sunet prin hdmi, trebuie sa urmez un ritual anume cu scos-bagat mufa de hdmi și schimbat sursa audio-out până funcționează;
  • nu am explorat prea mult, dar pare că windows nu este atât de personalizabil;
  • au revenit probleme precum: viruși și instalarea unui antivirus, vulnerabilități , privacy, BSOD, „google chrome has stopped working”;
  • pentru unele aplicații textul este randat ciudat din cauza problemor cu dpi;
  • sunt obișnuit să pornesc IDE direct în folder din linia de comandă. Pe Windows comanda pentru subline subl . deschide mai multe sesiuni de sublime … (later edit: am aflat din comentarii că acesta defapt este un feature);

Singurele motive pentru care Windows merită folosit: suita Office și programele VPN. Noroc că există VirtualBox cu guest additions!