Installation:
composer require avoo/achievement-bundle
Register the bundle in config/bundles.php:
return [
// ...
Avoo\AchievementBundle\AvooAchievementBundle::class => ['all' => true],
];
Configure User Entity:
Extend your User entity with UserInterface and add achievements relation:
use Avoo\AchievementBundle\Model\UserInterface;
class User implements UserInterface
{
// ...
/**
* @ORM\OneToMany(targetEntity="AppBundle\Entity\UserAchievement", mappedBy="user")
*/
private $achievements;
public function getAchievements() { return $this->achievements; }
}
Configure UserAchievement Entity:
Extend Avoo\AchievementBundle\Entity\UserAchievement and map it to your User:
use Avoo\AchievementBundle\Entity\UserAchievement as BaseUserAchievement;
class UserAchievement extends BaseUserAchievement
{
// ...
}
Basic Configuration:
Define the user_achievement_class in config/packages/avoo_achievement.yaml:
avoo_achievement:
user_achievement_class: App\Entity\UserAchievement
Trigger an achievement when a user completes a task (e.g., posts 5 articles):
// In your controller/service
$achievementService = $this->get('avoo_achievement');
$achievement = $achievementService->get('content.article_publisher');
$achievement->progress(1); // Increment progress by 1
Define Achievements:
Configure achievements in config/packages/avoo_achievement.yaml:
avoo_achievement:
achievements:
content:
article_publisher:
class: App\Listener\ArticlePublisherListener
name: "Published 5 Articles"
value: 5
description: "Unlocked after publishing 5 articles."
commenter:
class: App\Listener\CommentListener
name: "Frequent Commenter"
value: 20
description: "Unlocked after 20 comments."
Create Listeners:
Extend AchievementListener to handle logic:
namespace App\Listener;
use Avoo\AchievementBundle\Listener\AchievementListener;
class ArticlePublisherListener extends AchievementListener
{
public function isValid($user = null)
{
// Custom validation (e.g., check if user is active)
return $user && $user->isActive();
}
}
Trigger Achievements: Use events or services to update progress:
// In an event subscriber or service
public function onArticlePublished(ArticlePublishedEvent $event)
{
$user = $event->getUser();
$achievement = $this->achievementService->get('content.article_publisher');
$achievement->progress(1);
}
Display Achievements: Render achievements in Twig:
{% for category, achievements in achievements %}
<h2>{{ category }}</h2>
{% for achievement in achievements %}
<div class="achievement">
<h3>{{ achievement.name }}</h3>
<p>Progress: {{ achievement.progress }}/{{ achievement.value }}</p>
</div>
{% endfor %}
{% endfor %}
kernel.request, custom events).prePersist, preUpdate).// src/Controller/AchievementController.php
public function getProgress(User $user, string $achievementKey)
{
$achievement = $this->achievementService->get($achievementKey);
return $this->json([
'progress' => $achievement->getProgress($user),
'isComplete' => $achievement->isComplete($user),
]);
}
# config/packages/avoo_achievement.yaml
achievements:
content:
article_publisher:
name: "achievement.article_publisher.name"
# translations/messages.en.yaml
achievement:
article_publisher:
name: "Published 5 Articles"
Authentication:
isGranted('IS_AUTHENTICATED_FULLY') in controllers.public function showAchievements()
{
if (!$this->isGranted('IS_AUTHENTICATED_FULLY')) {
throw $this->createAccessDeniedException();
}
// ...
}
Progress Overwrite:
progress() multiple times without validation may overwrite progress. Use isValid() to gate progress updates:
if ($listener->isValid($user)) {
$listener->progress(1);
}
Configuration Overrides:
php bin/console cache:clear
Listener Instantiation:
Doctrine Mappings:
UserAchievement is properly mapped to User with inversedBy and mappedBy. Misconfigurations cause NULL relations.Check Progress: Query the database directly to verify progress:
SELECT * FROM avoo_user_achievement WHERE user_id = ? AND achievement_key = 'content.article_publisher';
Listener Debugging:
Override progress() to log progress:
public function progress($value)
{
$this->logger->info(sprintf(
'Progress updated for %s: %d/%d',
$this->getKey(),
$this->getProgress(),
$this->getValue()
));
return parent::progress($value);
}
Configuration Validation: Use Symfony’s validator to ensure config is correct:
php bin/console debug:config avoo_achievement
Bulk Progress Updates: Use transactions for batch updates (e.g., importing users):
$entityManager = $this->getDoctrine()->getManager();
$entityManager->beginTransaction();
try {
foreach ($users as $user) {
$achievement->progress(1, $user);
}
$entityManager->commit();
} catch (\Exception $e) {
$entityManager->rollBack();
throw $e;
}
Custom Achievement Types: Extend the bundle to support non-integer progress (e.g., time-based):
class TimeBasedAchievementListener extends AchievementListener
{
public function progress($timeSpentMinutes)
{
$current = $this->getProgress();
$this->setProgress($current + $timeSpentMinutes);
}
}
Achievement Badges: Serve achievement images dynamically:
// src/Controller/AchievementImageController.php
public function getImage(string $achievementKey)
{
$achievement = $this->achievementService->get($achievementKey);
$imagePath = $achievement->getImage();
return $this->file($imagePath);
}
Route:
# config/routes.yaml
achievement_image:
path: /achievement/image/{achievementKey}
controller: App\Controller\AchievementImageController::getImage
Testing: Mock the achievement service in tests:
$achievementService = $this->createMock(AchievementService::class);
$achievementService->method('get')->willReturn($this->createMock(AchievementListener::class));
$this->container->set('avoo_achievement', $achievementService);
Performance:
$this->cache->get('achievements_list', function() {
return $this->achievementService->getAll();
}, 3600);
$qb = $this->createQueryBuilder('ua')
->select
How can I help you explore Laravel packages today?