Skip to content

Commit

Permalink
#3 fetch articles from NewsApi
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyasAkbergen committed Jan 14, 2025
1 parent 8daa166 commit cbcae7b
Show file tree
Hide file tree
Showing 60 changed files with 1,898 additions and 102 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"

NEWSAPI_KEY=
NEWSAPI_BASE_URL=https://newsapi.org

XDEBUG_MODE=off
XDEBUG_PORT=
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ RUN apt-get update && apt-get install -y \
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Install Xdebug
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug
RUN pecl channel-update pecl.php.net && \
pecl install xdebug-3.4.0 && \
docker-php-ext-enable xdebug

# Copy existing application directory contents
COPY . /var/www/html
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ up:
docker compose up -d

test:
docker compose run --no-deps --rm php sh -c "composer install && php /var/www/html/vendor/phpunit/phpunit/phpunit --no-configuration /var/www/html/tests"
docker compose run --no-deps --rm php sh -c "touch database/database.sqlite && composer install && php /var/www/html/vendor/phpunit/phpunit/phpunit /var/www/html/tests"

phpstan:
docker compose run --no-deps --rm php sh -c "composer install && php /var/www/html/vendor/bin/phpstan"
43 changes: 43 additions & 0 deletions app/Factories/ArticleFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace App\Factories;

use App\Models\Article as ArticleModel;
use Domain\Entity\Article;
use Domain\Entity\Category;
use Domain\Entity\Source;
use Domain\Exception\DomainException;
use Domain\ValueObject\Url;

class ArticleFactory
{
/**
* @throws DomainException
*/
public static function fromEloquentModel(ArticleModel $model): Article
{
return new Article(
id: $model->getId(),
title: $model->title,
description: $model->description,
content: $model->content,
url: new Url($model->url),
imageUrl: $model->image_url !== null ? new Url($model->image_url) : null,
author: $model->author !== null ? AuthorFactory::fromEloquentModel($model->author) : null,
source: new Source(
id: $model->source->getId(),
code: $model->source->code,
name: $model->source->name,
),
category: new Category(
id: $model->category->getId(),
code: $model->category->code,
name: $model->category->name,
),
publishedAt: $model->published_at->toDateTimeImmutable(),
providerCode: $model->provider_code,
);
}
}
27 changes: 27 additions & 0 deletions app/Factories/AuthorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace App\Factories;

use App\Models\Author as AuthorEloquentModel;
use Domain\Entity\Author;
use Domain\Exception\DomainException;
use Domain\ValueObject\FullName;

class AuthorFactory
{
/**
* @throws DomainException
*/
public static function fromEloquentModel(AuthorEloquentModel $author): Author
{
return new Author(
id: $author->getId(),
fullName: new FullName(
firstName: $author->first_name,
lastName: $author->last_name,
),
);
}
}
4 changes: 2 additions & 2 deletions app/Jobs/FetchArticles.php → app/Jobs/FetchArticlesJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace App\Jobs;

use Domain\Service\ArticlesProvider;
use Domain\Service\ArticleProvider\ArticlesProvider;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class FetchArticles implements ShouldQueue
class FetchArticlesJob implements ShouldQueue
{
use Queueable;

Expand Down
107 changes: 107 additions & 0 deletions app/Jobs/SaveNewsApiArticleJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Services\ArticleProviders\NewsApi\CategoryFinder;
use App\Services\ArticleProviders\NewsApi\Dto\ArticleResponseDto;
use App\Services\ArticleProviders\NewsApi\SourceFinder;
use DateTimeImmutable;
use DateTimeInterface;
use Domain\Entity\Article;
use Domain\Entity\Author;
use Domain\Entity\Category;
use Domain\Entity\Source;
use Domain\Enum\ArticleProviderCode;
use Domain\Exception\DomainException;
use Domain\Exception\ExternalException;
use Domain\Repository\ArticleRepositoryInterface;
use Domain\Repository\AuthorRepositoryInterface;
use Domain\ValueObject\FullName;
use Domain\ValueObject\Url;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Ramsey\Uuid\Uuid;

class SaveNewsApiArticleJob implements ShouldQueue
{
use Queueable;

private AuthorRepositoryInterface $authorRepository;
private SourceFinder $sourceFinder;
private CategoryFinder $categoryFinder;

public function __construct(
public readonly ArticleResponseDto $articleResponseDto,
) {
}

/**
* @throws DomainException
* @throws ExternalException
*/
public function handle(
ArticleRepositoryInterface $articleRepository,
AuthorRepositoryInterface $authorRepository,
SourceFinder $sourceFinder,
CategoryFinder $categoryFinder,
): void {
$articleRepository1 = $articleRepository;
$this->authorRepository = $authorRepository;
$this->sourceFinder = $sourceFinder;
$this->categoryFinder = $categoryFinder;

$source = $this->getSource();
$article = new Article(
id: Uuid::uuid4(),
title: $this->articleResponseDto->title,
description: $this->articleResponseDto->description,
content: $this->articleResponseDto->content,
url: new Url($this->articleResponseDto->url),
imageUrl: $this->articleResponseDto->urlToImage !== null
? new Url($this->articleResponseDto->urlToImage)
: null,
author: $this->getAuthor(),
source: $source,
category: $this->getCategory($source),
publishedAt: DateTimeImmutable::createFromFormat(
DateTimeInterface::RFC3339,
$this->articleResponseDto->publishedAt,
),
providerCode: ArticleProviderCode::NEWS_API,
);

$articleRepository1->save($article);
}

/**
* @throws DomainException
*/
private function getAuthor(): ?Author
{
if ($this->articleResponseDto->author === null) {
return null;
}

return $this->authorRepository->findOrCreateByFullName(
FullName::fromString($this->articleResponseDto->author),
);
}

/**
* @throws ExternalException
*/
private function getSource(): Source
{
return $this->sourceFinder->findSource($this->articleResponseDto->source->id);
}

/**
* @throws ExternalException
*/
private function getCategory(Source $source): Category
{
return $this->categoryFinder->findBySource($source);
}
}
72 changes: 72 additions & 0 deletions app/Models/Article.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace App\Models;

use Domain\Enum\ArticleProviderCode;
use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

/**
* @property string $id
* @property string $title
* @property string $description
* @property string $content
* @property string $url
* @property string|null $image_url
* @property Carbon $published_at
* @property Source $source
* @property Author|null $author
* @property Category $category
* @property ArticleProviderCode $provider_code
*/
class Article extends Model
{
use HasTimestamps;
use HasUuids;

protected $fillable = [
'id',
'title',
'description',
'content',
'url',
'image_url',
'author_id',
'source_id',
'category_id',
'published_at',
'provider_code',
];

protected $casts = [
'published_at' => 'datetime',
'provider_code' => ArticleProviderCode::class,
];

public function getId(): UuidInterface
{
return $this->id instanceof UuidInterface ? $this->id : Uuid::fromString($this->id);
}

public function source(): BelongsTo
{
return $this->belongsTo(Source::class);
}

public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}

public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
41 changes: 41 additions & 0 deletions app/Models/Author.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

/**
* @property string $id
* @property string $first_name
* @property string $last_name
*/
class Author extends Model
{
use HasTimestamps;
use HasUuids;

protected $fillable = [
'id',
'first_name',
'last_name',
];

public static function createFromEntity(\Domain\Entity\Author $author): self
{
return new self([
'first_name' => $author->fullName->firstName,
'last_name' => $author->fullName->lastName,
]);
}

public function getId(): UuidInterface
{
return $this->id instanceof UuidInterface ? $this->id : Uuid::fromString($this->id);
}
}
33 changes: 33 additions & 0 deletions app/Models/Category.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

/**
* @property string $id
* @property string $code
* @property string $name
*/
class Category extends Model
{
use HasTimestamps;
use HasUuids;

protected $fillable = [
'id',
'code',
'name',
];

public function getId(): UuidInterface
{
return $this->id instanceof UuidInterface ? $this->id : Uuid::fromString($this->id);
}
}
Loading

0 comments on commit cbcae7b

Please sign in to comment.