/ Gists

Gists

On gists

24. Value -> Object

PHP Patterns

example1.php #

<?php
// https://forum.nette.org/cs/34970-rozne-ceny-za-roznych-okolnosti#p218919
class Product
{
	private Price $price;

	public function getPrice(): Price
	{
		return $this->price;
	}
}

//

	public function getPrice(...$modifiers): Price
	{
		$price = $this->price;
		foreach($modifiers as $modifier) {
			$price = $price->modify($modifier);
		}
		return $price;
	}
	
	//
	
	class PriceResolver
{
	private array $modifiers = [];

	public function setCustomer(IModifier $modifier): void
	{
		$this->modifiers[] = $modifier;
	}

	public function setSeason(IModifier $modifier): void
	{
		$this->modifiers[] = $modifier;
	}

	public function setOtherRule(IModifier $modifier): void
	{
		$this->modifiers[] = $modifier;
	}

	public function resolve(Product $product): Price
	{
		$price = $product->getPrice();
		foreach($modifiers as $modifier) {
			$price = $price->modify($modifier);
		}
		return $price;
	}
}

class Price
{
    private float $amount;
    private string $currency;

    public function __construct(float $amount, string $currency)
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function getAmount(int $precision = 2): float
    {
        return round($this->amount, $precision);
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    public function modify(Modifier $modifier): Price
    {
        if($modifier->isPercentage()) {
            $amount = $this->getAmount(4) * (100 + $modifier->getAmount()) / 100;
        }
        else {
            $amount = $this->getAmount(4) + $modifier->getAmount();
        }
        return new self($amount, $this->currency);
    }

    public function multiply(float $multiplier): Price
    {
        return new self($this->amount * $multiplier, $this->currency);
    }

    public function divide(float $divisor): Price
    {
        return new self($this->amount / $divisor, $this->currency);
    }

    public function add(Price $price): Price
    {
        if($this->currency !== $price->getCurrency()) {
            throw new CurrencyMismatchException("Currency missmatch");
        }
        return new self($this->amount + $price->getAmount(4), $this->currency);
    }

    public function minus(Price $price): Price
    {
        if($this->currency !== $price->getCurrency()) {
            throw new CurrencyMismatchException("Currency missmatch");
        }
        return new self($this->amount - $price->getAmount(4), $this->currency);
    }

    public function equals(Price $other): bool
    {
        return $this->getAmount(4) === $other->getAmount(4) and $this->currency === $other->getCurrency();
    }
}

class Modifier
{
    const TYPE_PERCENTAGE = "P";
    const TYPE_ABSOLUTE = "A";

    private string $type;
    private float $amount;

    public function __construct(string $type, float $amount)
    {
        $this->type = $type;
        $this->amount = $amount;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getAmount(int $precision = 2): float
    {
        return round($this->amount, $precision);
    }

    public function cumulate(Modifier $modifier): Modifier
    {
        if($this->type !== $modifier->getType()) {
            throw new TypeMissmatchException("Type missmatch");
        }
        return new self($this->type, $this->amount + $modifier->getAmount(4));
    }

    public function isPercentage(): bool
    {
        return $this->type === self::TYPE_PERCENTAGE;
    }

    public function isAbsolute(): bool
    {
        return $this->type === self::TYPE_ABSOLUTE;
    }
}

On gists

Nette 2.4 - custom filters - class

Nette Nette-Latte

SotioFilters.php #

<?php

namespace App\FrontModule\Filters;

use Nette;
use Andweb;

class SotioFilter
{

	/**
	 * Andweb\Localization\ITranslator
	 */
	private $translator;

	public function __construct(Andweb\Localization\ITranslator $translator)
	{
		$this->translator = $translator;
	}


	public function register($template)
    {
        $template->addFilter(null, [$this, 'loader']);
    }


	public function loader($filter)
	{
		if (method_exists(__CLASS__, $filter)) 
		{
            $args = func_get_args();
            array_shift($args);
            return call_user_func_array(array(__CLASS__, $filter), $args);
		}

		return NULL;
	}


	public function readMore($s)
	{
		$s = preg_replace('~(<div>--START--</div>)(.+)<div>--END--</div>~Usi', 
			'<div class="read-more-wrapper">$2</div><div class="read-more-btn-wrapper"><a href="">'.$this->translator->translate('READ MORE').'</a></div>',
			 $s
		);

		return $s;
	}


}

On gists

$attrs

Vue.js

index.vue #

<template>
  <div>
    <p>
      <custom-checkbox>Simple example</custom-checkbox>
    </p>
    <p>
      <custom-checkbox :disabled="true">Disabled</custom-checkbox>
    </p>
  </div>
</template>

<script>
import CustomCheckbox from './CustomCheckbox'
export default {
  components: { CustomCheckbox }
}
</script>

<style scoped>

</style>

On gists

Recursion

Vue.js

index.vue #

<template>
  <div>
    <tree-list
      :children="treeItems"
      :parents="[]"
    />
  </div>
</template>

<script>
  import TreeList from './TreeList.vue'

  export default {
    components: {TreeList},
    compontents: {
      TreeList
    },
    data () {
      return {
        treeItems: [
          {
            title: 'First top level',
            children: [
              {
                title: 'First sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              },
              {
                title: 'Second sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              },
              {
                title: 'Third sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              }
            ]
          },
          {
            title: 'Second top level',
            children: [
              {
                title: 'First sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              },
              {
                title: 'Second sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              },
              {
                title: 'Third sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              }
            ]
          },
          {
            title: 'Third top level',
            children: [
              {
                title: 'First sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              },
              {
                title: 'Second sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              },
              {
                title: 'Third sub level',
                children: [
                  {title: 'First sub-sub level'},
                  {title: 'Second sub-sub level'},
                  {title: 'Third sub-sub level'}
                ]
              }
            ]
          }
        ]
      }
    }
  }
</script>

<style scoped>

</style>

On gists

V-model in loops and computed

Vue.js

App.vue #

/* 
  https://stackblitz.com/edit/superlasice-efzixd?file=src%2FApp.vue 
*/

<template>
  <div id="app">
    <button v-on:click="add">add new row</button>
    <p>Total price {{ total }} | {{ total2 }}</p>

    <ul>
      <li v-for="(item, index) in items">
        Name<br />
        <input type="text" v-model="item.name" />
        <br />

        Quantity<br />
        <input type="number" v-model.number="item.quantity" min="1" />
        <br />

        Price<br />
        <input
          type="number"
          v-model.number="item.price"
          min="0.00"
          max="10000"
          step="1"
        />
        <br />

        Total (readonly) <br />
        <input v-model="totalItems[index]" readonly /> <br />

        total in row:
        {{ totalItem(item) }}
        <br />
        <br />

        <button v-on:click="remove(index)">Delete row</button>
        <hr />
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      items: [
        { name: 'A', quantity: 1, price: 20 },
        { name: 'B', quantity: 2, price: 30 },
      ],
    };
  },
  methods: {
    add() {
      this.items.push({
        name: 'New product',
        quantity: 0,
        price: 0,
      });
    },
    remove(index) {
      this.items.splice(index, 1);
    },
    totalItem(item) {
      return item.price * item.quantity;
    },
  },
  computed: {
    totalItems() {
      let arr = [];
      this.items.forEach((item) => {
        arr.push(parseFloat(item.price) * parseFloat(item.quantity));
      });

      return arr;
    },
    total() {
      let sum = 0;
      this.items.forEach((item) => {
        sum += parseFloat(item.price) * parseFloat(item.quantity);
      });

      return sum;
    },
    // another approach how to sum
    total2() {
      return this.items.reduce((prev, item) => {
        return prev + item.price * item.quantity;
      }, 0);
    },
  },
};
</script>

On gists

Abort exception

Nette Nette-Tricks

abort.php #

<?php


try
		{
		  // ...
			$this->redirect('PresenterWhatEver:default');

		}
		catch (\Exception $e)
		{
			// posle se to dal kdyz premserujeme - redirect vyhazuje abortException ...
			if ($e instanceof Nette\Application\AbortException)
			{
				throw $e;
			}
		}

On gists

Resize window

Vue.js

resize-window.vue #

methods: {
  resizeWindow() {
    window.dispatchEvent(new Event('resize'))
  }
},
mounted() {
  this.resizeWindow()
}

On gists

Vue directive + class tooltip (4 learning)

Vue.js

directive.js #

/* https://github.com/hekigan/vue-directive-tooltip */


/*
 * @author: laurent blanes <laurent.blanes@gmail.com>
 * @tutorial: https://hekigan.github.io/vue-directive-tooltip/
 */
import Tooltip from './tooltip.js';

const BASE_CLASS = 'vue-tooltip';
const POSITIONS = ['auto', 'top', 'bottom', 'left', 'right'];
const SUB_POSITIONS = ['start', 'end'];

/**
 * usage:
 *
 * // basic usage:
 * <div v-tooltip="'my content'">
 * or
 * <div v-tooltip="{content: 'my content'}">
 *
 * // change position of tooltip
 * // options: auto (default) | bottom | top | left | right
 *
 * // change sub-position of tooltip
 * // options: start | end
 *
 * <div v-tooltip.top="{content: 'my content'}">
 *
 * // add custom class
 * <div v-tooltip="{class: 'custom-class', content: 'my content'}">
 *
 * // toggle visibility
 * <div v-tooltip="{visible: false, content: 'my content'}">
 */
export default {
    name: 'tooltip',
    config: {},
    install (Vue, installOptions) {
        Vue.directive('tooltip', {
            bind (el, binding, vnode) {
                if (installOptions) {
                    Tooltip.defaults(installOptions);
                }
            },
            inserted (el, binding, vnode, oldVnode) {
                if (installOptions) {
                    Tooltip.defaults(installOptions);
                }

                let options = filterBindings(binding, vnode);
                el.tooltip = new Tooltip(el, options);

                if (binding.modifiers.notrigger && binding.value.visible === true) {
                    el.tooltip.show();
                }

                if (binding.value && binding.value.visible === false) {
                    el.tooltip.disabled = true;
                }
            },
            componentUpdated (el, binding, vnode, oldVnode) {
                if (hasUpdated(binding.value, binding.oldValue)) {
                    update(el, binding, vnode, oldVnode);
                }
            },
            unbind (el, binding, vnode, oldVnode) {
                el.tooltip.destroy();
            }
        });
    }
};

/**
 *
 * @param {*} vnode component's properties
 * @param {*} oldvnode component's previous properties
 * @return boolean
 */
function hasUpdated (value, oldValue) {
    let updated = false;

    if (typeof value === 'string' && value !== oldValue) {
        updated = true;
    } else if (isObject(value)) {
        Object.keys(value).forEach(prop => {
            if (value[prop] !== oldValue[prop]) {
                updated = true;
            }
        });
    }
    return updated;
}

/**
 * Sanitize data
 * @param {*} binding
 * @param {*} vnode
 * @return {*} filtered data object
 */
function filterBindings (binding, vnode) {
    const delay = !binding.value || isNaN(binding.value.delay) ? Tooltip._defaults.delay : binding.value.delay;

    if (binding.value.ref) {
        if (vnode.context.$refs[binding.value.ref]) {
            binding.value.html = vnode.context.$refs[binding.value.ref];
        } else {
            console.error(`[Tooltip] no REF element [${binding.value.ref}]`); // eslint-disable-line
        }
    }

    return {
        class: getClass(binding),
        id: (binding.value) ? binding.value.id : null,
        html: (binding.value) ? binding.value.html : null,
        placement: getPlacement(binding),
        title: getContent(binding),
        triggers: getTriggers(binding),
        fixIosSafari: binding.modifiers.ios || false,
        offset: (binding.value && binding.value.offset) ? binding.value.offset : Tooltip._defaults.offset,
        delay
    };
}

/**
 * Get placement from modifiers
 * @param {*} binding
 */
function getPlacement ({modifiers, value}) {
    let MODS = Object.keys(modifiers);
    if (MODS.length === 0 && isObject(value) && typeof value.placement === 'string') {
        MODS = value.placement.split('.');
    }
    let head = 'bottom';
    let tail = null;
    for (let i = 0; i < MODS.length; i++) {
        const pos = MODS[i];
        if (POSITIONS.indexOf(pos) > -1) {
            head = pos;
        }
        if (SUB_POSITIONS.indexOf(pos) > -1) {
            tail = pos;
        }
    }
    // console.log((head && tail) ? `${head}-${tail}` : head);
    // return 'auto';
    return (head && tail) ? `${head}-${tail}` : head;
}

/**
 * Get trigger value from modifiers
 * @param {*} binding
 * @return String
 */
function getTriggers ({modifiers}) {
    let trigger = [];
    if (modifiers.notrigger) {
        return trigger;
    } else if (modifiers.manual) {
        trigger.push('manual');
    } else {
        if (modifiers.click) {
            trigger.push('click');
        }

        if (modifiers.hover) {
            trigger.push('hover');
        }

        if (modifiers.focus) {
            trigger.push('focus');
        }

        if (trigger.length === 0) {
            trigger.push('hover', 'focus');
        }
    }

    return trigger;
}

/**
 * Check if the variable is an object
 * @param {*} value
 * @return Boolean
 */
function isObject (value) {
    return typeof value === 'object';
}

/**
 * Check if the variable is an html element
 * @param {*} value
 * @return Boolean
 */
function isElement (value) {
    return value instanceof window.Element;
}

/**
 * Get the css class
 * @param {*} binding
 * @return HTMLElement | String
 */
function getClass ({value}) {
    if (value === null) {
        return BASE_CLASS;
    } else if (isObject(value) && typeof value.class === 'string') {
        return `${BASE_CLASS} ${value.class}`;
    } else if (Tooltip._defaults.class) {
        return `${BASE_CLASS} ${Tooltip._defaults.class}`;
    } else {
        return BASE_CLASS;
    }
}

/**
 * Get the content
 * @param {*} binding
 * @return HTMLElement | String
 */
function getContent ({value}, vnode) {
    if (value !== null && isObject(value)) {
        if (value.content !== undefined) {
            return `${value.content}`;
        } else if (value.id && document.getElementById(value.id)) {
            return document.getElementById(value.id);
        } else if (value.html && document.getElementById(value.html)) {
            return document.getElementById(value.html);
        } else if (isElement(value.html)) {
            return value.html;
        } else if (value.ref && vnode) {
            return vnode.context.$refs[value.ref] || '';
        } else {
            return '';
        }
    } else {
        return `${value}`;
    }
}

/**
 * Action on element update
 * @param {*} el Vue element
 * @param {*} binding
 */
function update (el, binding, vnode, oldVnode) {
    if (typeof binding.value === 'string') {
        el.tooltip.content(binding.value);
    } else {
        if (binding.value && binding.value.class && binding.value.class.trim() !== el.tooltip.options.class.replace(BASE_CLASS, '').trim()) {
            el.tooltip.class = `${BASE_CLASS} ${binding.value.class.trim()}`;
        }

        el.tooltip.content(getContent(binding, vnode));

        if (!binding.modifiers.notrigger && binding.value && typeof binding.value.visible === 'boolean') {
            el.tooltip.disabled = !binding.value.visible;
            return;
        } else if (binding.modifiers.notrigger) {
            el.tooltip.disabled = false;
        }

        const dir = vnode.data.directives[0];

        if (dir.oldValue.visible !== dir.value.visible) {
            if (!el.tooltip.disabled) {
                el.tooltip.toggle(dir.value.visible);
            }
        }
    }
}

On gists

Pivot table via prepared statements

MySql MySql tricks MySql - advanced

pivot.sql #

create table tbl_values (
  id int unsigned not null primary key,
  `Name` varchar(10),
  `Group` varchar(10),
  `Value` int
);

insert into tbl_values values
(1, 'Pete', 'A', 10),
(2, 'Pete', 'B', 20),
(3, 'John', 'A', 10);

-- 1. Create an expression that builds the columns
set @sql = (
    select group_concat(distinct 
        concat(
            "sum(case when `Group`='", `Group`, "' then `Value` end) as `", `Group`, "`"
        )
    ) 
    from tbl_values
);

-- 2. Complete the SQL instruction
set @sql = concat("select Name, ", @sql, " from tbl_values group by `Name`");

-- 3. Create a prepared statement
prepare stmt from @sql;

-- 4. Execute the prepared statement
execute stmt;

On gists

OnInvalidClick - Nette form

Nette-Forms AW

onInvalidClick.php #

<?php
  // https://git.andweb.cz/brandcloud/app/-/blob/bcnew/app/BrandCloudModule/components/SharingForm.php#L148

	$form->addSubmit('share', 'Share')
			->onInvalidClick[] = function () use ($form) {
				// we have to set recipients as items to revalidate recipients field
				// TODO: posibly change multiselectbox to some other implementation which works only with raw value
				if ($form['sharing_type']->getValue() === 'email') {
					$form['recipients']->setItems((array) $form['recipients']->getRawValue(), false);
					$form->validate();
				}
			};

		$form->onValidate[] = [$this, 'validateForm'];
		$form->onSubmit[] = [$this, 'submitForm'];
		$form->onSuccess[] = [$this, 'successForm'];
		$form->setDefaults($this->getFormDefaults());
		return $form;