Для получения полного доступа
зарегистрируйтесь.

samdark    349   100,503


Engineer, active OpenSource member, one of Yii core team members and its representative in PHP-FIG.Wrote a book.

  • Voronezh
  • Yii, Stay.com
  • Yii, PHP, JavaScript, Java, Android, SQL, OOP, OOD
  • VSU, Computer Science, master
  • Registered 5 years ago
Snippet,  PHP

mb_ucfirst

Mulibyte versions of ucfirst() which works well with unicode.

function mb_ucfirst($string, $encoding = 'UTF-8') {
  $firstChar = mb_strtoupper(mb_substr($string, 0, 1, $encoding), $encoding);
  return $firstChar . mb_substr($string, 1, mb_strlen($string, $encoding), $encoding);
}

It's totally irritating when you have Mapbox or Leaflet map on a long long page. When you're scrolling map comes into the view it is starting to zoom map out instead of scrolling the page.

It could be fixed by requiring three second hover over the map before it could be scrolled.

var map = L.mapbox.map('mymap', 'mymap');

// disable scroll-zooming initially
map.scrollWheelZoom.disable();

var zoomTimer;

// on entering the map we're starting a timer to
// 3 seconds after which we'll enable scroll-zooming
map.on('mouseover', function() {
	zoomTimer = setTimeout(function(){
		map.scrollWheelZoom.enable();
    }, 3000);
});

// on leaving the map we're disarming not yet triggered timers
// and disabling scrolling
map.on('mouseout', function() {
    clearTimeout(zoomTimer);
    map.scrollWheelZoom.disable();
});

After posting about fast hydration I've got response with even faster method that doesn't require extra class. Credits go to Vladimir Chub.

$postHydrator = function(array $data) {
    $this->id = $data['id'];
    $this->title = $data['title'];
    $this->text = $data['text'];
};

$postExtractor = function() {
    return [
        'id' => $this->id,
        'title' => $this->title,
        'text' => $this->text
    ];
}

// $data is from database

$post = new Post();
$hydrator = $postHydrator->bindTo($post, $post);
$hydrator($data);

It's important for object oriented programming and domain driven design not to violate incapsulation i.e. what should not be set by developer should be private or protected:

class Post
{
    protected $id;
    protected $title;
    protected $text;

    public function getId()
    {
        return $this->id;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getText()
    {
        return $this->text;
    }

    public function setText($text)
    {
        $this->text = $text;
    }
}

When it comes to pesisting data in database and getting it back there's a problem of filling properties we can't set. Usually it's solved by using reflection but it's quite slow when it comes to hundreds of objects. Marco Pivetta posted a better approach which despite being meant for Doctrine internals is quite simple and interesting.

class PostSelfHydratingProxy extends Post
{
    /** zero-argument constructor since this is just a helper class for hydration */
    public function __construct()
    {
    }
    
    public function hydrate($data, Post $object)
    {
		$object->id = $data['id'];
        $object->title = $data['title'];
        $object->text = $data['text'];
    }

    
    public function extract(Post $object)
    {
        return [
            'id' => $object->id,
            'title' => $object->title,
            'text' => $object->text
        ];
    }
}

// $data is from database

$post = new Post();
$proxy = new PostSelfHydratingProxy();
$proxy->hydrate($data, $post);

It's possible to shuffle array once a week without storing anything. I've found the solution at StackOverflow.

It's based on initializing random number generator which is used by shuffle() with a seed that's constant for each week. date('W') is a week number in a year which fits the purpose well. After shuffling is done RNG is put back to normal by calling srand() with no argument i.e. initilizing it with random seed.

srand(date('W'));
shuffle($array);
srand();

In many cases it's useful to know if environment uses Suhosin.

if (extension_loaded('suhosin')) {
   echo 'Installed as PHP extension';
} elseif (defined("SUHOSIN_PATCH")) {
   echo 'Patched into PHP';
} else {
   echo 'Not detected';
}

Despite it's quite common requirement to assert that elements in two arrays are the same while order doesn't matter, it's not that obvious on how to do it in PHPUnit.

$array = ['b', 'a'];
$this->assertEquals(['a', 'b'], $array, '', 0.0, 10, true);

Since PHP 5.4 it's possible to get script execution time without prior recording of script initial timestamp.

echo microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"];

In order to delete item from fragment cache in Yii 2.0 you need to form a key in a special way.

function deleteFragmentCacheByKey($key)
{
    return Yii::$app->cache->delete(['yii\widgets\FragmentCache', $key]);
}

A short and reliable way to detect if the script is run in Windows environment.

if (DIRECTORY_SEPARATOR === '\\') {
    // ...
}

There are cases when you need to make sure keys specified are in the beginning of an array in the exact order they are specified.

$data = [
    'orange' => 'orange',
    'apple' => 'tasty',
    'carpet' => 'old',
    'car' => 'fast',
];

$result = orderByKeys($data, ['car', 'carpet']);

Would result in:

$data = [
    'car' => 'fast',
    'carpet' => 'old',
    'orange' => 'orange',
    'apple' => 'tasty',    
];
function orderByKeys(array $array, array $keys)
{
    foreach ($keys as $k => $v) {
        if (!array_key_exists($v, $array)) {
            unset($keys[$k]);
        }
    }

   return array_replace(array_flip($keys), $array);
}

PHP has excellenet intl extension which is based on ICU library for algorithms and CLDR data for locale data. The code shows how to get currency symbol by currency code using intl.

Example usage:

echo getCurrencySymbol('EUR');
echo getCurrencySymbol('GBP');

would output and £.

By specifying locale code as a second argument you can get currency representation that is common for that locale:

echo getCurrencySymbol('RUR', 'ru_RU');

would output р..

function getCurrencySymbol($currencyCode, $locale = 'en_US')
{
    $formatter = new \NumberFormatter($locale . '@currency=' . $currencyCode, \NumberFormatter::CURRENCY);
    return $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL);
}

The query is an efficient way to get 10 latest comments limiting it to maximum one comment per post.

SELECT *
FROM comment c1
LEFT JOIN comment c2 ON c1.id < c2.id AND c1.post_id = c2.post_id
WHERE c2.id IS NULL
ORDER BY c1.id DESC
LIMIT 10

While it's kinda pointless, it's possible to use callbacks as URL handlers totally avoiding MVC in the same style as many micro-frameworks do.

require 'vendor/autoload.php';
require 'vendor/yiisoft/yii2/Yii.php';


$app = new CallbackWebApplication([
    'id' => 'MyApplication',
    'basePath' => __DIR__,
    'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
        ],
    ],
]);

$app->onGet('hello/<name>', function($name) {
    return 'Hello, ' . $name . '!';
});
$app->run();
<?php
class CallbackWebApplication extends \yii\web\Application
{
    private $callbacks = [];

    /**
     * @inheritdoc
     */
    public function handleRequest($request)
    {
        list ($route, $params) = $request->resolve();
        if (preg_match('~callbacks/(\d+)~', $route, $matches) && isset($this->callbacks[$matches[1]])) {
            $result = call_user_func_array($this->callbacks[$matches[1]], $params);
        } else {
            throw new \yii\web\NotFoundHttpException();
        }

        $response = $this->getResponse();
        $response->data = $result;
        return $response;
    }

    public function onGet($pattern, callable $callback)
    {
        // it's possible to implement our own UrlRule class to handle it
        // but I was too lazy
        $callbackIndex = count($this->callbacks);
        $this->callbacks[$callbackIndex] = $callback;
        Yii::$app->getUrlManager()->addRules([
            $pattern => 'callbacks/' . $callbackIndex
        ]);
    }
}

The approach demonstrated is used in my Sitemap package to allow generating huge sitemaps consisting of hundreds thousangs of locations.

// we'll buffer data in memory in order not to write too often
$bufferSize = 1000;

$filePath = 'test.xml';

$writer = new XMLWriter();
// using memory for buffering
$writer->openMemory();


$writer->startDocument('1.0', 'UTF-8');
$writer->setIndent(true);
$writer->startElement('test');

while ($i < 10000) {                  
	if ($i % $bufferSize) {
		// if buffer is full, flush its contents into the file
		file_put_contents($filePath, $writer->flush(true), FILE_APPEND);
	}
	$writer->writeElement('item', $i);
}

$this->writer->endElement();

$this->writer->endDocument();

// write the rest of the buffer
file_put_contents($filePath, $writer->flush(true), FILE_APPEND);

Thanks to closures we can avoid using global flag which spoils the scope.

The code could be executed in any browser console. To trigger it, click anywhere on the page.

window.addEventListener('click', function () {
    var i = 0;
    return function () {
        window.alert('You\'ve clicked it ' + (++i) + ' time(s).');
    };
}());
Snippet,  PHP

Types and in_array

$array = array('one', 'two');
var_dump(in_array(0, $array)); // true
var_dump(in_array(0, $array, $strict = true)); // false

Named scopes are quite useful and are familiar to developers who work with Yii. The code shows how to get these in CodeIgniter.

class Post extends Model
{
    function __construct() {
        parent::__construct();
        $this->db->from('post')->order_by("created_at", "desc");
    }
 
    function limit($number) {
        $this->db->limit($number);
        return $this;
    }
 
    function all() {
        return $this->db->get()->result();        
    }
}

$posts = $this->post->limit(10)->find();
foreach($posts as $post) {
	// ..
SELECT *
FROM news
WHERE id IN (2, 10, 3, 88, 23)
ORDER BY FIELD (id, 2, 10, 3, 88, 23)

Safari and old Chrome are triggering additional popstate event on page load. Modern Chrome fixed it but it still happens in current Safari.

The idea is to register a handler after initial popstate pops. It is triggered on load so we're adding handler for it. setTimeout with 0 delay is to make sure handler registered last.

$(window).load(function() {
    setTimeout(function() {
        $(window).on('popstate', function (e) {
                // handle it
        });
    }, 0);
});