Готова первая версия агрегатора купонов http://otkuponer.ru/ (если не открывается, ждите пока приедут DNS-ы).
Как обычно минимальный дизайн. На текущий момент работает 2 системы купонов(биглион и групон), на подходе купикупон.
Проект выполняется в чистом стиле проектирования по модели предметной области, но без Active Record (ty to CanceRus). Для разработки используется Agile подход.
СУБД юзается MongoDB, программировать мега-приятно (ty to 10gen).
Фреймворк для разработки — старичок CodeIgniter (но по скорости работы он весьма не старичок).
В планах сделать парсеры на 100500 сайтов купонов и что-нить улучшить для посетителей.
Фреймворк CodeIgniter из коробки позволяет расположить контроллеры только в одноуровневой папке: dir/file/class/params
Порой случается нужно сделать dir многоуровневой, например: admin/panel/sites/user/add
При попытке обратиться к странице получим ошибку, файл контроллера не найден.
Решение (для CI 2.0, 2.1):
Создать файл /application/core/MY_Router.php
Скопировать в него:
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); /** * MY_Router Class * * An extension to the core router class to allow controllers to be in multi-level directories. * * @package Router * @version 1.0 * @author Damien K. * @copyright Copyright (c) 2010, Ollie Rattue * @license http://www.opensource.org/licenses/mit-license.php * @link https://github.com/ollierattue/codeigniter-multi-level-controller-extension */ class MY_Router extends CI_Router { // -------------------------------------------------------------------- /** * OVERRIDE * * Validates the supplied segments. Attempts to determine the path to * the controller. * * @access private * @param array * @return array */ function _validate_request($segments) { if (count($segments) == 0) { return $segments; } // Does the requested controller exist in the root folder? if (file_exists(APPPATH.'controllers/'.$segments[0].EXT)) { return $segments; } // Is the controller in a sub-folder? if (is_dir(APPPATH.'controllers/'.$segments[0])) { // @edit: Support multi-level sub-folders $dir = ''; do { if (strlen($dir) > 0) { $dir .= '/'; } $dir .= $segments[0]; $segments = array_slice($segments, 1); } while (count($segments) > 0 && is_dir(APPPATH.'controllers/'.$dir.'/'.$segments[0])); // Set the directory and remove it from the segment array $this->set_directory($dir); // @edit: END // @edit: If no controller found, use 'default_controller' as defined in 'config/routes.php' if (count($segments) > 0 && ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments[0].EXT)) { array_unshift($segments, $this->default_controller); } // @edit: END if (count($segments) > 0) { // Does the requested controller exist in the sub-folder? if (!file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments[0].EXT)) { // show_404($this->fetch_directory().$segments[0]); // @edit: Fix a "bug" where show_404 is called before all the core classes are loaded $this->directory = ''; // @edit: END } } else { // Is the method being specified in the route? if (strpos($this->default_controller, '/') !== FALSE) { $x = explode('/', $this->default_controller); $this->set_class($x[0]); $this->set_method($x[1]); } else { $this->set_class($this->default_controller); $this->set_method('index'); } // Does the default controller exist in the sub-folder? if (!file_exists(APPPATH.'controllers/'.$this->fetch_directory().$this->default_controller.EXT)) { $this->directory = ''; return array(); } } return $segments; } // If we've gotten this far it means that the URI does not correlate to a valid // controller class. We will now see if there is an override if (!empty($this->routes['404_override'])) { $x = explode('/', $this->routes['404_override']); $this->set_class($x[0]); $this->set_method(isset($x[1]) ? $x[1] : 'index'); return $x; } // Nothing else to do at this point but show a 404 show_404($segments[0]); } // -------------------------------------------------------------------- /** * OVERRIDE * * Set the directory name * * @access public * @param string * @return void */ function set_directory($dir) { $this->directory = str_replace(array('.'), '', $dir).'/'; // @edit: Preserve '/' } // -------------------------------------------------------------------- }
Благодарность https://github.com/ollierattue/codeigniter-multi-level-controller-extension
Многобуков. Всё под катом.
Качаем ericmann-gReader-Library с гитхаба.
Сразу проезжаемся рубанком, заменяя private на protected, иначе нельзя обратиться к методам и свойствам при расширении класса extends.
Задача: вытянуть из Google Reader записи, отмеченные звёздочками (starred) и отослать информацию о снятии звёздочки
Решение, расширяем базовый класс двумя методами:
include_once 'ericmann-gReader-Library/greader.class.php'; class google_reader extends JDMReader { public function __construct() { } public function connect($username, $password) { parent::__construct($username, $password); } // List a particular number of unread posts from the user's reading list public function getStarred($limit) { $gUrl = 'http://www.google.com/reader/api/0/stream/contents/user/-/state/com.google/starred'; $args = sprintf('n=%2$s&ck=%3$s&client=GoogleReaderDashboard', time() - (7*24*3600), $limit, time()); $data = $this->_httpGet($gUrl, $args); $decoded_data = json_decode($data, true); $feed_items = $decoded_data['items']; return($feed_items); } /** * Mark this an item as unstarred * * This method marks an item as unstarred for a certain user. * * @param string $itemId The item id that can be retrieved from $this->listAll() * * @return boolean */ public function markAsUnstarred($itemId) { $data = sprintf( 'i=%1$s&T=%2$s&r=%3$s&ac=edit', $itemId, $this->_token,'user/-/state/com.google/starred' ); $path = '/reader/api/0/edit-tag?client=-'; $host = 'www.google.com'; $response = $this->_httpPost($host, $path, $data); if($response == null) return false; return true; } }
юзаем:
$this->load->library('google_reader'); $this->google_reader->connect('login@gmail.com','******'); if ($this->google_reader->loaded) { print_r($this->google_reader->getStarred(10)); }
Реализация доставляет. Даже observer натянули на библиотеку.
Неоптимальная либа.
На каждое обращение к либе — обращение к базе, часто с join по всем трём таблицам.
Пользуюсь библиотекой от Алекса Биби для работы с MongoDB в стиле active record. Мне там не хватало пары функций. Добавил их.
За основу взята эта штука http://alexbilbie.blogs.lincoln.ac.uk/2010/04/17/a-codeigniter-active-record-library-for-mongodb/
Но в ней не хватало методов выборки первой записи удовлетворяющей критерию и метода замены объекта в коллекции
Добавлены методы:
get_one — использует метод findone. Формат вызова get_one($collection = «»)
Пример: @usage = $this->mongo_db->where(array(‘bar’ => ‘something’))->get_one(‘foo’);
replace — заменяет элемент на новый. Формат вызова replace($collection = «», $data = array())
Пример: @usage = $this->mongo_db->where(array(‘bar’ => ‘something’))->replace(‘foo’, $data = array());
Скачать: mongo_db_driver.rar
В продолжение решения задачи: Реляционка или не реляционка
Пройдя первый этап: MySQL vs MongoDB. Сравнение скорости insert Создания пользователей.
Пройдя второй этап:MySQL vs MongoDB. Более сложный insert (для mongodb) Создания друзей.
Пройдя третий этап: MySQL vs MongoDB. Более сложный insert (для mongodb) часть 2. Создания фотографий у каждого пользователя
Теперь пометим у каждого френда (100 френдов у каждого из 1000 пользователей) по 50 фотографий из 100 просмотренными. Получается 1000*100*50=5000000 (пять миллионов записей):
Для MySQL это вставка в таблицу who_view_photo записи с id_photo и id_user просмотревшем его.

Для MongoDB это изменение существующих объектов в коллекции пользователей. Причём мы меняем 3-й уровень объекта в глубину:
Посмотрим насколько шустро отработает наш MongoDB. Ему уже приходится в каждом объекте содержать информацию о 100 друзьях и 100 фотографиях (1000 записей).
По оси Х-время, по оси Y-количество случаев с данным временем. На примере вставки первых 50000 элементов (просмотров фотографий):
Вывод: Объекты MongoDB разжирнев, обрабатываются дольше, чем банальная вставка значения в таблицу MySQL. Но даже содержа 1000 значений в объекте, выполняя фактически операцию update к объекту (меняя его содержимое) мы получаем все-лишь двукратное замедление, что радует.
В продолжение решения задачи: Реляционка или не реляционка
Пройдя первый этап: MySQL vs MongoDB. Сравнение скорости insert Создания пользователей.
Пройдя второй этап:MySQL vs MongoDB. Более сложный insert (для mongodb) Создания друзей.
Создадим фотографии пользователей. Каждому пользователю по 100 фотографий.
Для MySQL это обычное создание записи в таблице:
Для NoSQL это добавление в коллекцию пользователей из MySQL vs MongoDB. Более сложный insert (для mongodb) ещё одного поля photos, являющегося множеством элементов.
И сравниваем производительность выполнения вставки 100 фотографий каждому из 1000 пользователей (100000 элементов).
По оси Х-время в секундах, по оси Y-количество случаев с данным временем выполнения(основной кусок увеличен):
Вывод: нужно понимать, что для MySQL — создание фотографии лишь вставка новой записи в таблицу фотографий, а для MongoDB — изменение существующего объекта, в котором уже содержится 100 друзей, созданных в предыдущем этапе, и ещё добавляется 100 элементов фотографий. Но при всём этом MongoDB половину операций (~50000) отработало за 0.0001 секунды, а MySQL редко ранее 0.0005 секунды. И MongoDB пришлось проделать гораздо большую работу (обновить существующие объекты).
В продолжение решения задачи: Реляционка или не реляционка
Пройдя первый этап: MySQL vs MongoDB. Сравнение скорости insert
Создадим для 1000 пользователей системы по 100 друзей каждому (1000*100=100000 записей).
В MySQL мы имеем структуру:

Таким образом для MySQL это будет вставка записи в таблицу friends.
В MongoDB мы имеем структуру сложнее. т.к. вся информация представлена в виде большого дерева.
Мы имеем коллекцию пользователей. В которой все наши 1000 пользователей как документы.
Каждый документ имеет структуру:
Понять сложно, если вы до этого ни разу не делали nosql проект. Обязательно изучите.
Ну так вот, операция создания френдов, по времени (по оси Х-время в секундах, по оси Y-количество операций с этим временем выполнения):
Вывод: тут нужно понимать, что в mysql — обычная вставка данных в таблицу, а в MongoDB — обновление существующего объекта — создание в его поле ifriend ещё одного элемента, хоть и с одним значением id_friend, но всё-же это update. Ну и мы видим, что выполнив 100000 таких операций мы имеет сравнимо одинаковые затраты по времени.
Попробуем решить интересную задачу двумя способами (реляционным и не реляционным).
Допустим есть социальная сеть.
В ней есть пользователи, которые помечают других пользователей френдами.
Пользователи выкладывают какие-либо фотографии.
Задача: Для текущего пользователя вывести список всех фотографий френдов, которые этот пользователь ещё не просмотрел. О_о
Бум использовать:
php фреймворк CodeIgniter
mysql для реляционки
MongoDB для NoSql
Начальные данные:
1. создадим 1000 пользователей
2. для каждого пользователя зададим 100 друзей
3. для каждого пользователя зададим 100 фотографий (получится всего 100,000 фотографий О_о)
4. каждым пользователем посетим случайные 50 фото каждого своего друга (50*100*1000=5,000,000 записей)
5. каждым пользователем посетим случайные 1000 фото (1,000*1,000=1,000,000 ненужных записей дополнительно)
Ну и область наших интересов — быстродействие
Пока в процессе проектирования — пишите что-нить =)