Skip to main content
This guide covers best practices for writing efficient, safe, and maintainable PocketMine-MP plugins.

Performance Optimization

Cache Frequently Accessed Data

Don’t repeatedly access configs or perform expensive operations:
// ❌ Bad - reads config every time
public function canBreakBlock(Player $player) : bool {
    return $player->hasPermission($this->getConfig()->get("break-permission"));
}

// ✅ Good - cache in onEnable()
private string $breakPermission;

protected function onEnable() : void {
    $this->breakPermission = $this->getConfig()->get("break-permission", "plugin.break");
}

public function canBreakBlock(Player $player) : bool {
    return $player->hasPermission($this->breakPermission);
}

Avoid Heavy Operations in Events

Event handlers should be fast, especially for frequent events:
// ❌ Bad - expensive operation in PlayerMoveEvent
public function onMove(PlayerMoveEvent $event) : void {
    // This fires 20+ times per second per player!
    $this->checkAllRegions($event->getPlayer());
    $this->updateDatabase($event->getPlayer());
}

// ✅ Good - use scheduled tasks for heavy work
private array $playersToCheck = [];

public function onMove(PlayerMoveEvent $event) : void {
    // Queue player for checking
    $this->playersToCheck[$event->getPlayer()->getName()] = true;
}

protected function onEnable() : void {
    // Check players once per second instead
    $this->getScheduler()->scheduleRepeatingTask(
        new ClosureTask(function() : void {
            foreach($this->playersToCheck as $name => $_) {
                $player = $this->getServer()->getPlayerByPrefix($name);
                if($player !== null) {
                    $this->checkAllRegions($player);
                }
            }
            $this->playersToCheck = [];
        }),
        20
    );
}

Use Event Priority Wisely

/**
 * Use HIGH priority for expensive checks that might be unnecessary
 * if another plugin cancels the event
 * @priority HIGH
 */
public function onBlockBreak(BlockBreakEvent $event) : void {
    // Runs after most other plugins
    if($event->isCancelled()) {
        return;  // Skip expensive check
    }
    
    $this->doExpensiveCheck($event->getBlock());
}

Batch Database Operations

// ❌ Bad - query for each player
foreach($players as $player) {
    $this->database->savePlayer($player);
}

// ✅ Good - batch save
$this->database->beginTransaction();
foreach($players as $player) {
    $this->database->savePlayer($player);
}
$this->database->commit();

Thread Safety

Never Access Server from Async Tasks

// ❌ WRONG - Will crash!
class BadAsyncTask extends AsyncTask {
    public function onRun() : void {
        // Server is NOT available in async tasks!
        $this->plugin->getServer()->broadcastMessage("Hi");
    }
}

// ✅ Correct - use onCompletion()
class GoodAsyncTask extends AsyncTask {
    
    public function onRun() : void {
        // Do work here (no Server access)
        $result = $this->calculateSomething();
        $this->setResult($result);
    }
    
    public function onCompletion() : void {
        // Server IS available here (runs on main thread)
        $plugin = $this->fetchLocal("plugin");
        $plugin->getServer()->broadcastMessage("Task done!");
    }
}

Store Plugin Reference Properly in Async Tasks

class MyAsyncTask extends AsyncTask {
    
    public function __construct(Main $plugin) {
        // Use storeLocal for non-serializable objects
        $this->storeLocal("plugin", $plugin);
    }
    
    public function onCompletion() : void {
        /** @var Main $plugin */
        $plugin = $this->fetchLocal("plugin");
        $plugin->handleResult($this->getResult());
    }
}

Don’t Share Data Between Threads

// ❌ Bad - shared array (not thread-safe)
private array $sharedData = [];

public function onEnable() : void {
    $this->getServer()->getAsyncPool()->submitTask(
        new class($this->sharedData) extends AsyncTask {
            // Dangerous! Array is shared between threads
        }
    );
}

// ✅ Good - pass serializable data
public function startTask(array $data) : void {
    $serialized = json_encode($data);
    $this->getServer()->getAsyncPool()->submitTask(
        new class($serialized) extends AsyncTask {
            public function __construct(
                private string $data
            ){}
            
            public function onRun() : void {
                $data = json_decode($this->data, true);
                // Work with data
            }
        }
    );
}

Common Pitfalls

Not Checking if Player is Online

// ❌ Bad - player might have disconnected
$this->getScheduler()->scheduleDelayedTask(
    new ClosureTask(function() use ($player) : void {
        $player->sendMessage("Hi!");  // Might crash!
    }),
    100
);

// ✅ Good - check if player is still online
$this->getScheduler()->scheduleDelayedTask(
    new ClosureTask(function() use ($player) : void {
        if($player->isConnected()) {
            $player->sendMessage("Hi!");
        }
    }),
    100
);

// ✅ Better - use player name
$playerName = $player->getName();
$this->getScheduler()->scheduleDelayedTask(
    new ClosureTask(function() use ($playerName) : void {
        $player = $this->getServer()->getPlayerExact($playerName);
        if($player !== null) {
            $player->sendMessage("Hi!");
        }
    }),
    100
);

Not Validating User Input

// ❌ Bad - no validation
public function onCommand(CommandSender $sender, Command $cmd, string $label, array $args) : bool {
    $amount = (int) $args[0];
    $this->giveMoney($sender, $amount);
    return true;
}

// ✅ Good - validate input
public function onCommand(CommandSender $sender, Command $cmd, string $label, array $args) : bool {
    if(count($args) < 1) {
        $sender->sendMessage("Usage: /command <amount>");
        return false;
    }
    
    if(!is_numeric($args[0])) {
        $sender->sendMessage("§cAmount must be a number");
        return true;
    }
    
    $amount = (int) $args[0];
    
    if($amount <= 0 || $amount > 1000000) {
        $sender->sendMessage("§cAmount must be between 1 and 1000000");
        return true;
    }
    
    $this->giveMoney($sender, $amount);
    return true;
}

Memory Leaks from Event Handlers

// ❌ Bad - stores references to all players
private array $players = [];

public function onJoin(PlayerJoinEvent $event) : void {
    $this->players[] = $event->getPlayer();
    // Player objects never removed - memory leak!
}

// ✅ Good - use player names or clean up on quit
private array $playerData = [];

public function onJoin(PlayerJoinEvent $event) : void {
    $name = $event->getPlayer()->getName();
    $this->playerData[$name] = ["joinTime" => time()];
}

public function onQuit(PlayerQuitEvent $event) : void {
    unset($this->playerData[$event->getPlayer()->getName()]);
}

Forgetting to Save Data

// ❌ Bad - data lost on crash
public function setPlayerScore(Player $player, int $score) : void {
    $this->scores[$player->getName()] = $score;
    // Not saved!
}

// ✅ Good - save important data immediately
public function setPlayerScore(Player $player, int $score) : void {
    $this->scores[$player->getName()] = $score;
    $this->saveScores();
}

// ✅ Or use auto-save task
protected function onEnable() : void {
    $this->getScheduler()->scheduleRepeatingTask(
        new ClosureTask(function() : void {
            $this->saveScores();
        }),
        1200  // Auto-save every minute
    );
}

protected function onDisable() : void {
    $this->saveScores();  // Always save on disable
}

Code Organization

Separate Concerns

// ❌ Bad - everything in Main class
class Main extends PluginBase implements Listener {
    public function onJoin(PlayerJoinEvent $event) : void { }
    public function onBlockBreak(BlockBreakEvent $event) : void { }
    public function onCommand(...) : bool { }
    public function saveData() : void { }
    public function loadData() : void { }
    // ... 500 more lines
}

// ✅ Good - organized into classes
class Main extends PluginBase {
    private EventListener $listener;
    private DataManager $dataManager;
    private CommandHandler $commandHandler;
    
    protected function onEnable() : void {
        $this->dataManager = new DataManager($this);
        $this->listener = new EventListener($this);
        $this->commandHandler = new CommandHandler($this);
        
        $this->getServer()->getPluginManager()->registerEvents(
            $this->listener, 
            $this
        );
    }
}

class EventListener implements Listener {
    public function onJoin(PlayerJoinEvent $event) : void { }
    public function onBlockBreak(BlockBreakEvent $event) : void { }
}

class DataManager {
    public function save() : void { }
    public function load() : void { }
}

class CommandHandler {
    public function handleCommand(...) : bool { }
}

Use Manager Classes

class Main extends PluginBase {
    private PlayerManager $playerManager;
    private GameManager $gameManager;
    private EconomyManager $economyManager;
    
    protected function onEnable() : void {
        $this->playerManager = new PlayerManager($this);
        $this->gameManager = new GameManager($this);
        $this->economyManager = new EconomyManager($this);
    }
    
    public function getPlayerManager() : PlayerManager {
        return $this->playerManager;
    }
    
    public function getGameManager() : GameManager {
        return $this->gameManager;
    }
}

Error Handling

Always Handle Exceptions

// ❌ Bad - crashes plugin on error
public function loadData() : void {
    $data = json_decode(file_get_contents($this->getDataFolder() . "data.json"), true);
}

// ✅ Good - handle errors gracefully
public function loadData() : void {
    try {
        $file = $this->getDataFolder() . "data.json";
        
        if(!file_exists($file)) {
            $this->getLogger()->warning("Data file not found, using defaults");
            return;
        }
        
        $contents = file_get_contents($file);
        if($contents === false) {
            throw new \RuntimeException("Failed to read data file");
        }
        
        $data = json_decode($contents, true);
        if($data === null) {
            throw new \RuntimeException("Invalid JSON in data file");
        }
        
        $this->data = $data;
        
    } catch(\Exception $e) {
        $this->getLogger()->error("Failed to load data: " . $e->getMessage());
        $this->getLogger()->logException($e);
    }
}

Log Errors Properly

// Different log levels
$this->getLogger()->debug("Debug information");
$this->getLogger()->info("Normal information");
$this->getLogger()->notice("Important information");
$this->getLogger()->warning("Warning - something might be wrong");
$this->getLogger()->error("Error - something went wrong");
$this->getLogger()->critical("Critical - plugin cannot function");
$this->getLogger()->emergency("Emergency - server-wide issue");

// Log exceptions
try {
    // code
} catch(\Exception $e) {
    $this->getLogger()->logException($e);
}

Security

Validate Permissions

// Always check permissions
public function onCommand(CommandSender $sender, Command $cmd, string $label, array $args) : bool {
    if(!$sender->hasPermission("myplugin.admin")) {
        $sender->sendMessage("§cNo permission");
        return true;
    }
    
    // Command logic
    return true;
}

Sanitize User Input

// ❌ Bad - SQL injection possible
$name = $args[0];
$query = "SELECT * FROM players WHERE name='$name'";

// ✅ Good - use prepared statements
$name = $args[0];
$stmt = $db->prepare("SELECT * FROM players WHERE name = ?");
$stmt->execute([$name]);

Don’t Trust Client Data

public function onBlockPlace(BlockPlaceEvent $event) : void {
    $player = $event->getPlayer();
    
    // ❌ Bad - trust client position
    $pos = $event->getBlock()->getPosition();
    
    // ✅ Good - validate position is near player
    $pos = $event->getBlock()->getPosition();
    if($pos->distance($player->getPosition()) > 10) {
        $event->cancel();
        $this->getLogger()->warning(
            $player->getName() . " tried to place block too far away"
        );
    }
}

Testing and Debugging

Add Debug Mode

class Main extends PluginBase {
    private bool $debug;
    
    protected function onEnable() : void {
        $this->saveDefaultConfig();
        $this->debug = $this->getConfig()->get("debug", false);
    }
    
    public function debug(string $message) : void {
        if($this->debug) {
            $this->getLogger()->debug($message);
        }
    }
}

// Usage:
$this->debug("Player " . $player->getName() . " entered zone " . $zoneId);

Use Type Declarations

// ✅ Good - strict types and type hints
declare(strict_types=1);

class Main extends PluginBase {
    
    public function teleportPlayer(Player $player, Position $pos) : bool {
        // Types are enforced
        return $player->teleport($pos);
    }
    
    private function calculateScore(int $kills, int $deaths) : float {
        if($deaths === 0) return (float) $kills;
        return $kills / $deaths;
    }
}

Performance Monitoring

class Main extends PluginBase {
    
    public function measurePerformance(callable $callback, string $label) : mixed {
        $start = microtime(true);
        $result = $callback();
        $time = (microtime(true) - $start) * 1000;
        
        if($time > 50) {  // Log if > 50ms
            $this->getLogger()->warning(
                "$label took {$time}ms (slow!)"
            );
        }
        
        return $result;
    }
}

// Usage:
$this->measurePerformance(
    fn() => $this->loadAllData(),
    "Data loading"
);

Summary Checklist

Performance:
  • Cache frequently accessed data
  • Avoid heavy operations in event handlers
  • Use appropriate event priorities
  • Batch database operations
Thread Safety:
  • Never access Server in AsyncTask::onRun()
  • Use storeLocal/fetchLocal for plugin references
  • Don’t share mutable data between threads
Safety:
  • Check if players are online in delayed tasks
  • Validate all user input
  • Clean up event listener references
  • Always save data in onDisable()
Code Quality:
  • Separate code into logical classes
  • Handle exceptions gracefully
  • Use proper logging levels
  • Add type declarations
  • Validate permissions
Testing:
  • Add debug mode
  • Monitor performance
  • Test with multiple players
  • Test plugin reload/disable

Next Steps

Getting Started

Create your first plugin

Event Handlers

Learn about events

Build docs developers (and LLMs) love