diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..d6af03b4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+.idea/
+code/vendor/
diff --git a/README.md b/README.md
deleted file mode 100644
index 6490de86..00000000
--- a/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# PHP2021
\ No newline at end of file
diff --git a/code/app.php b/code/app.php
new file mode 100644
index 00000000..289e1685
--- /dev/null
+++ b/code/app.php
@@ -0,0 +1,11 @@
+run();
+}
+catch(Exception $e) {
+ App\Response::generateBadRequestResponse($e->getMessage());
+}
\ No newline at end of file
diff --git a/code/app_src/Application.php b/code/app_src/Application.php
new file mode 100644
index 00000000..973bf526
--- /dev/null
+++ b/code/app_src/Application.php
@@ -0,0 +1,60 @@
+ElasticSearchInterface = new ESStorage();
+ $this->statisticsManager = new StatisticsManager($this->ElasticSearchInterface);
+
+ if (!RequestValidator::checkMainFields($_POST, $this->ElasticSearchInterface)) {
+ throw new \Exception('Main fields missing or empty');
+ }
+
+ $this->request = $_POST;
+ }
+
+ public function run()
+ {
+ switch($this->request['tab-btn']) {
+ case 'add':
+ $result = $this->ElasticSearchInterface->insert($this->request);
+ break;
+
+ case 'delete':
+ $result = $this->ElasticSearchInterface->delete($this->request);
+ break;
+
+ case 'info':
+ $result = $this->statisticsManager->getChannelSummary($this->request['id']);
+ break;
+
+ case 'top':
+ $result = $this->statisticsManager->getTopRatedChannels();
+ break;
+
+ default:
+ throw new \Exception('Empty request');
+ break;
+ }
+
+ print_r($result);
+ }
+}
\ No newline at end of file
diff --git a/code/app_src/Interface/StorageInterface.php b/code/app_src/Interface/StorageInterface.php
new file mode 100644
index 00000000..2d534237
--- /dev/null
+++ b/code/app_src/Interface/StorageInterface.php
@@ -0,0 +1,12 @@
+getMainFields() as $field) {
+ if (!isset($request[$field]) || empty($request[$field])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/code/app_src/Response.php b/code/app_src/Response.php
new file mode 100644
index 00000000..e7396d3c
--- /dev/null
+++ b/code/app_src/Response.php
@@ -0,0 +1,18 @@
+storageInterface = $storageInterface;
+ }
+
+ public function getChannelSummary(string $channelId) : array
+ {
+ $arChannelVideos = $this->storageInterface->search(
+ [
+ 'index' => 'youtube_video',
+ 'body' => [
+ 'query' => [
+ 'match' => [
+ 'channel_id' => $channelId
+ ]
+ ]
+ ]
+ ]
+ );
+
+ $likesCount = 0;
+ $dislikesCount = 0;
+ foreach ($arChannelVideos as $singleVideo) {
+ $likesCount += $singleVideo['_source']['likes'];
+ $dislikesCount += $singleVideo['_source']['dislikes'];
+ }
+
+ return [
+ 'likes' => $likesCount,
+ 'dislikes' => $dislikesCount,
+ ];
+ }
+
+ public function getTopRatedChannels() : array
+ {
+ $arRate = [];
+ $arChannels = $this->storageInterface->search(
+ [
+ 'index' => 'youtube_channel',
+ ]
+ );
+
+ foreach ($arChannels as $channel) {
+ $arChannelSummary = $this->getChannelSummary($channel['_source']['channel_id']);
+ try {
+ $arRate[$channel['_source']['channel_name']] = $arChannelSummary['likes'] / $arChannelSummary['dislikes'];
+ } catch (Exception $e) {
+ $arRate[$channel['_source']['channel_name']] = PHP_MAX_INT;
+ }
+ }
+
+ asort($arRate, SORT_NUMERIC);
+ return $arRate;
+ }
+}
\ No newline at end of file
diff --git a/code/app_src/Storage/ESStorage.php b/code/app_src/Storage/ESStorage.php
new file mode 100644
index 00000000..5b51b371
--- /dev/null
+++ b/code/app_src/Storage/ESStorage.php
@@ -0,0 +1,82 @@
+client = ClientBuilder::create()->setHosts($this->hosts)->build();
+ }
+
+ public function insert(string $request) : array
+ {
+ return $this->client->index($this->getRequestBody($request));
+ }
+
+ public function delete(string $request)
+ {
+ return $this->client->delete($this->getRequestBody($request));
+ }
+
+ public function search(string $arData) : array
+ {
+ return $this->client->search($arData)['hits']['hits'];
+ }
+
+ private function getRequestBody(array $request) : array
+ {
+ switch ($request['index']) {
+ case 'youtube_channel':
+ return $this->makeChannelArray($request);
+
+ case 'youtube_video':
+ return $this->makeVideoArray($request);
+
+ default:
+ throw new \Exception('Wrong index passed');
+
+ }
+ }
+
+ private function makeChannelArray(array $request) : array
+ {
+ return [
+ 'index' => $request['index'],
+ 'id' => $request['id'],
+ 'body' => [
+ 'channel_id' => $request['id'],
+ 'channel_name' => $request['name']
+ ]
+ ];
+ }
+
+ private function makeVideoArray($request)
+ {
+ return [
+ 'index' => $request['index'],
+ 'id' => $request['id'],
+ 'body' => [
+ 'video_id' => $request['id'],
+ 'title' => $request['name'],
+ 'likes' => $request['video_likes'],
+ 'dislikes' => $request['video_dislikes'],
+ 'channel_id' => $request['video_channel_id'],
+ ]
+ ];
+ }
+
+ public function getMainFields() : array
+ {
+ return $this->mainFields;
+ }
+}
\ No newline at end of file
diff --git a/code/composer.json b/code/composer.json
new file mode 100644
index 00000000..0cd02e36
--- /dev/null
+++ b/code/composer.json
@@ -0,0 +1,14 @@
+{
+"require": {
+"php": "~7.4",
+"ext-json": "*",
+"ext-sockets": "*",
+"elasticsearch/elasticsearch": "^7.0",
+"google/apiclient": "~2.0"
+},
+"autoload": {
+"psr-4": {
+"App\\": "app_src"
+}
+}
+}
diff --git a/code/index.html b/code/index.html
new file mode 100644
index 00000000..c2b53546
--- /dev/null
+++ b/code/index.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+ Title
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..ec04ca55
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,92 @@
+version: '3'
+
+services:
+ nginx-balancer:
+ build:
+ context: ./nginx-balancer
+ dockerfile: Dockerfile
+ container_name: nginx-balancer
+ image: localapp/nginx-balancer
+ ports:
+ - "80:80"
+ - "443:443"
+ depends_on:
+ - nginx-webserver1
+ - nginx-webserver2
+ networks:
+ - app-network
+
+ nginx-webserver1:
+ build:
+ context: ./nginx-webserver
+ dockerfile: Dockerfile
+ container_name: nginx-webserver1
+ image: localapp/nginx-webserver
+ ports:
+ - "8081:80"
+ volumes:
+ - ./code:/var/www/mysite.local
+ networks:
+ - app-network
+
+ nginx-webserver2:
+ build:
+ context: ./nginx-webserver
+ dockerfile: Dockerfile
+ container_name: nginx-webserver2
+ image: localapp/nginx-webserver
+ ports:
+ - "8082:80"
+ volumes:
+ - ./code:/var/www/mysite.local
+ networks:
+ - app-network
+
+ php1:
+ build:
+ context: ./php-fpm
+ dockerfile: Dockerfile
+ image: localapp/php
+ container_name: php1
+ volumes:
+ - ./code:/var/www/mysite.local
+ networks:
+ - app-network
+
+ php2:
+ build:
+ context: ./php-fpm
+ dockerfile: Dockerfile
+ image: localapp/php
+ container_name: php2
+ volumes:
+ - ./code:/var/www/mysite.local
+ networks:
+ - app-network
+
+ elastic_search:
+ image: docker.elastic.co/elasticsearch/elasticsearch:7.16.3
+ container_name: elastic_search
+ ports:
+ - "9200:9200"
+ - "9300:9300"
+ environment:
+ - "discovery.type=single-node"
+ networks:
+ - app-network
+
+ kibana:
+ image: docker.elastic.co/kibana/kibana:7.16.3
+ container_name: kibana
+ ports:
+ - "5601:5601"
+ environment:
+ - "ELASTICSEARCH_HOSTS=http://elastic_search:9200"
+ networks:
+ - app-network
+ depends_on:
+ - elastic_search
+
+networks:
+ app-network:
+ driver: bridge
\ No newline at end of file
diff --git a/nginx-balancer/Dockerfile b/nginx-balancer/Dockerfile
new file mode 100644
index 00000000..cf52d591
--- /dev/null
+++ b/nginx-balancer/Dockerfile
@@ -0,0 +1,11 @@
+FROM ubuntu:latest
+
+RUN apt-get update && apt-get install -y nginx
+
+COPY ./hosts/nginx-balancer.local.conf /etc/nginx/sites-enabled/mysite.local.conf
+
+WORKDIR /var/www/mysite.local
+VOLUME /var/www/mysite.local
+EXPOSE 80
+
+CMD [ "nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/nginx-balancer/hosts/nginx-balancer.local.conf b/nginx-balancer/hosts/nginx-balancer.local.conf
new file mode 100644
index 00000000..79aa339f
--- /dev/null
+++ b/nginx-balancer/hosts/nginx-balancer.local.conf
@@ -0,0 +1,19 @@
+upstream nginx-webservers {
+ server nginx-webserver1;
+ server nginx-webserver2;
+}
+
+server {
+ listen 80;
+
+ server_name mysite.local;
+ error_log /var/log/nginx/error.log;
+ access_log /var/log/nginx/access.log;
+
+ location / {
+ proxy_pass http://nginx-webservers;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
diff --git a/nginx-webserver/Dockerfile b/nginx-webserver/Dockerfile
new file mode 100644
index 00000000..91fe15c6
--- /dev/null
+++ b/nginx-webserver/Dockerfile
@@ -0,0 +1,11 @@
+FROM ubuntu:latest
+
+RUN apt-get update && apt-get install -y nginx
+
+COPY ./hosts/mysite.conf /etc/nginx/sites-enabled/mysite.conf
+
+WORKDIR /var/www/mysite.local
+VOLUME /var/www/mysite.local
+EXPOSE 80
+
+CMD [ "nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/nginx-webserver/hosts/mysite.conf b/nginx-webserver/hosts/mysite.conf
new file mode 100644
index 00000000..b64247ee
--- /dev/null
+++ b/nginx-webserver/hosts/mysite.conf
@@ -0,0 +1,67 @@
+upstream php-fpm {
+
+ server php1:9000;
+
+ server php2:9000;
+
+}
+
+
+
+
+server {
+
+ listen 80;
+
+ index index.php index.html index.htm;
+
+ server_name mysite.local;
+
+ root /var/www/mysite.local;
+
+ error_log /var/log/nginx/error.log;
+
+ access_log /var/log/nginx/access.log;
+
+
+
+
+ location ~* .(jpg|jpeg|gif|css|png|js|html)$ {
+
+ access_log off;
+
+ expires max;
+
+ }
+
+
+
+
+ location / {
+
+ try_files $uri $uri/ /index.php?$query_string;
+
+ }
+
+
+
+
+ location ~* .php$ {
+
+ try_files $uri = 404;
+
+ fastcgi_split_path_info (.+?\.php)(/.*)$;
+
+ fastcgi_pass php-fpm;
+
+ fastcgi_index index.php;
+
+ include fastcgi_params;
+
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+
+ }
+
+}
\ No newline at end of file
diff --git a/php-fpm/Dockerfile b/php-fpm/Dockerfile
new file mode 100644
index 00000000..cffffe45
--- /dev/null
+++ b/php-fpm/Dockerfile
@@ -0,0 +1,30 @@
+FROM php:8.0-fpm
+
+RUN apt-get update && apt-get install -y \
+ curl \
+ wget \
+ git \
+ libfreetype6-dev \
+ libjpeg62-turbo-dev \
+ libpng-dev \
+ libonig-dev \
+ libzip-dev \
+ libmcrypt-dev \
+ libmemcached-dev\
+ libmemcached-tools\
+ && pecl install memcached \
+ && docker-php-ext-enable memcached \
+ && pecl install mcrypt-1.0.4 \
+ && docker-php-ext-enable mcrypt \
+ && docker-php-ext-install -j$(nproc) iconv mbstring mysqli pdo_mysql zip \
+ && docker-php-ext-configure gd --with-freetype --with-jpeg \
+ && docker-php-ext-install -j$(nproc) gd \
+ && curl -sS https://getcomposer.org/installer \
+ | php -- --install-dir=/usr/local/bin --filename=composer
+
+ADD php.ini /usr/local/etc/php/conf.d/40-custom.ini
+
+WORKDIR /var/www
+VOLUME /var/www
+
+CMD ["php-fpm"]
\ No newline at end of file
diff --git a/php-fpm/php.ini b/php-fpm/php.ini
new file mode 100644
index 00000000..6dccaac2
--- /dev/null
+++ b/php-fpm/php.ini
@@ -0,0 +1,3 @@
+extension=memcached.so
+session.save_handler = memcache
+session.save_path = "tcp://memcache:11211"
\ No newline at end of file