Tutorial Handling Data with Data Objects and Managing Player Sessions

Discussion in 'Resources' started by SOFe, Dec 10, 2016.

  1. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    When a plugin stores some information for a player, it usually wants to store more than one piece of data. Even if you only plan to store one datum, who knows if you won't store more in the future?
    To prevent trouble refactoring (i.e. renaming function/variable names, changing their expected values, etc.) your code in the future, and to store multiple pieces of data effectively, you should use data classes to represent each set of data.
    Using data objects instead of using a Config object also allows you to support multiple database types.

    In this thread, I am making a plugin which stores the player's number of joins, total online time and last online date.
    Therefore, a player's data object should contain these three data, and we make a class like this:
    (Remember, always create a new file to put a new class in it according to PSR-0, i.e. folder path == namespace and file name = class name + ".php")
    PHP:
    class PlayerData{
        private 
    $joins;
        private 
    $onlineTime;
        private 
    $lastOnline;
    }
    In most programs, instead of exposing their variables (actually called class properties) directly, we have getter functions and setter functions, which will change/return the class properties. This allows you to save anything you like in your class properties, as long as you return something valid in your getters. So when you change what you store in the class properties (for example, if you suddenly want to save the online time in milliseconds instead of seconds), other plugins using your plugin's API functions will not be affected if you correct the values in the getter functions.
    So we are setting the class properties as `private` (i.e. other classes can't change it), and adding getters (I'll leave out setters for the while since what we want is not exactly as simple as setting the value):

    PHP:
    class PlayerData{
        private 
    $joins;
        private 
    $onlineTime;
        private 
    $lastOnline;

        public function 
    getJoins() : int{
            return 
    $this->joins;
        }

        public function 
    getOnlineTime() : float{
            return 
    $this->onlineTime;
        }

        public function 
    getLastOnline() : float{
            return 
    $this->lastOnline;
        }
    }
    Then we can write `$data = new PlayerData();` to make a new object that contains these fields. But they don't have default values set; let's add the default values.
    PHP:
    class PlayerData{
        private 
    $joins 0;
        private 
    $onlineTime 0;
        private 
    $lastOnline = -1;

        public function 
    getJoins() : int{
            return 
    $this->joins;
        }

        public function 
    getOnlineTime() : float{
            return 
    $this->onlineTime;
        }

        public function 
    getLastOnline() : float{
            return 
    $this->lastOnline;
        }
    }
    What about making it like `$data = new PlayerData("username")'`, so that $data will be filled with the data for the played called "username"?
    It is actually the same as loading the data first then put them into an array. But if we put the code inside PlayerData.php, it looks more organized, because the code for loading player data belongs to the PlayerData class, not anywhere else like event handlers. In high-quality plugins, you shouldn't see code that directly loads data or saves data in command executors or event handlers.

    We can add action for the `new PlayerData(...)` call by making a method in the PlayerData class called __construct, with a parameter for the username:

    PHP:
    class PlayerData{
        
    // These two class properties are not used for changeable variables, but
        // it is best if we keep a reference to these values so that we can know
        // what this object is about just by passing this object around.
        
    private $main;
        private 
    $username;

        private 
    $joins 0;
        private 
    $onlineTime 0;
        private 
    $lastOnline 0;

        public function 
    __construct(MainClass $mainstring $username){ // we need to pass the main class instance too so that PlayerData knows which directory to save in
            // my practice is to always put these lines for defining immutable (cannot be
            // changed) class properties in the beginning of a constructor, because the
            // following method calls may use them.
            
    $this->main $main;
            
    $this->username $username;

            
    // I am making the path into a function because I know that
            // we will use it again later, when we save data.
            // So if we change the file path, we don't need to change two places, just one place. This avoids so many strange bugs
            
    $path $this->getPath();
            if(!
    is_file($path)){ // if the file doesn't exist, i.e. player never joined this server after this plugin is installed
                
    return; // do nothing, because we will leave everything to their default values
            
    }

            
    // load the data in the YAML file into memory!
            
    $data yaml_parse_file($path); // use this instead of `(new Config(...))->getAll()`!
            
    $this->joins $data["joins"];
            
    $this->onlineTime $data["onlineTime"];
            
    $this->lastOnline $data["lastOnline"];
        }

        public function 
    getPath() : string{
            
    // Hint: remember to strtolower(), because some filesystems are case-sensitive!
            
    return $this->main->getDataFolder() . strtolower($this->username) . ".yml";
        }

        public function 
    getJoins() : int{
            return 
    $this->joins;
        }

        public function 
    getOnlineTime() : float{
            return 
    $this->onlineTime;
        }

        public function 
    getLastOnline() : float{
            return 
    $this->lastOnline;
        }
    }
    So let's add the save function too:

    PHP:
    public function save(){
        
    yaml_emit_file($this->getPath(), ["joins" => $this->joins"onlineTime" => $this->onlineTime"lastOnline" => $this->lastOnline]);
    }
    You may be asking, why do we need to use objects if we finally have to make it into an associative array like in the last line?
    This is actually because using associative arrays is only needed for particular formats. For example, if you are storing data using the NBT format (the verbosity is quite pointless though), or in an SQL database, you won't need to convert them into associative arrays, but something else instead.

    Now, in our main class, we should have an array that is going to be filled with these PlayerData. Remember, our server runs for indefinite time; it could stop anytime, and it could run virtually forever if we design it well. So we should maintain this array (which will be reset every time plugin reloads or server restarts) at a definite size to avoid wasting memory, but not rely on this array forever.

    In case you do not know how this array can be used, I am going to add a PlayerData object into this array once a player joins, and delete it from the array once the player quits. Why keep it in an array? Because we may want to use this object anytime when the player is online. For example, if we are saving the number of kills of the player, we would add one to the variable every time the player kills. We only do disk I/O when player quits, or periodically if you are scared about server crashes (but not in this thread's scope of discussion).

    To retrieve the PlayerData from the array quickly, we would index the array using something unique for the player, for example, the username. However, I prefer using player entity IDs for two reasons:
    1. Using integers (entity IDs) is faster than using strings (names). But this doesn't consist of a real reason, because premature optimization should not be considered when there are other things to consider about.
    2. Player entity IDs are different in different sessions (change after player rejoins). So in case something strange happens and I am editing the player data even after the player has left, I would only modify the old object and not affecting the new object. Wouldn't this make bugs less visible? Yes, but if you don't overwrite the old PlayerData entry, at least you can still find it out from the memory "oh wtf why isn't this deleted already" when you var_dump() your array.

    So our main class should have this:
    PHP:
    class MainClass extends PluginBase implements Listener{
        private 
    $playerData = [];

        
    // other stuff

        
    public function onJoin(PlayerJoinEvent $event){
            
    $player $event->getPlayer();
            
    $this->playerData[$player->getId()] = new PlayerData($this$player->getName());
        }

        public function 
    onQuit(PlayerQuitEvent $event){
            
    $player $event->getPlayer();
            
    // remember to check this! If player quits before he joins the server, or
            // if he is banned/whitelisted/server full etc., PlayerQuitEvent will fire without PlayerJoinEvent first!
            
    if(isset($this->playerData[$player->getId()])){
                
    $this->playerData[$player->getId()]->save();
                unset(
    $this->playerData[$player->getId()]);
            }
        }
    }
    TO BE CONTINUED.
     
    Last edited: Dec 13, 2016
  2. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    Now I'm going to talk about manipulating the values. It is actually really simple.
    As I said, we create getter functions instead of editing the class properties directly (like $data->joins = 0;) because we don't want to expose the "internal" values to other plugins directly, but instead through methods where we can do something before the value is modified/returned.
    Actually, there is no reason why other plugins want to modify the value apart from incrementing the joins, or adding something to the online time, or change the last online time to present. (Actually there can be -- they may want to reset the value, for example -- but let's not put too much thought into this simple example plugin)

    PHP:
        public function incrementJoins(){
            
    $this->joins++;
        }
     
        public function 
    addOnlineTime(float $add) : float{
            
    $this->onlineTime += $add;
        }
        public function 
    updateLastOnline(){
            
    $this->lastOnline microtime(true);
        }
    Now let's implement these methods in our main class! The joins and update last online are simple. Let me change them like this...

    PHP:
        public function onJoin(PlayerJoinEvent $event){
            
    $player $event->getPlayer();
            
    $this->playerData[$player->getId()] = $data = new PlayerData($this$player->getName());
            
    $data->incrementJoins();
        }

        public function 
    onQuit(PlayerQuitEvent $event){
            
    $player $event->getPlayer();
            
    // remember to check this! If player quits before he joins the server, or
            // if he is banned/whitelisted/server full etc., PlayerQuitEvent will fire without PlayerJoinEvent first!
            
    if(isset($this->playerData[$player->getId()])){
                
    $data $this->playerData[$player->getId()];
                
    $data->updateLastOnline();
                
    $data->save();
                unset(
    $this->playerData[$player->getId()]);
            }
        }
    Now... how do we count the overall online time of the player? Hmm... We have to store the join time of the player, and recalculate the online time of the player every time it is checked. But we don't save the join time of the player into the data files. So let's just put a temporary field in the PlayerData class for the join time:
    PHP:
        private $main;
        private 
    $username;

        private 
    $joins 0;
        private 
    $onlineTime 0;
        private 
    $lastOnline 0;

        private 
    $lastLoadTime;
    For reasons that you will know soon, I am calling it "lastLoadTime" instead of "lastJoinTime". You will know why soon.
    I want to set lastLoadTime default to the time the PlayerData is constructed (when the player starts getting online), but because PHP is stupider than Java, we can't set the default class property values in the declaration directly if it is not constant , so let's do it in the constructor:
    PHP:
    public function __construct(MainClass $mainstring $username){
        
    // ...
        
    $this->lastLoadTime microtime(true);
    }
    Since lastLoadTime is an internal value, we don't need to create getters and setters for it. Actually, I'm going to modify getLastJoinTime(), so whenever someone uses getOnlineTime(), it will add the time the player has been online to the onlineTIme:
    PHP:
    public function getOnlineTime() : float{
            
    $micro microtime(true);
            
    $this->onlineTime += $micro $this->lastLoadTime;
            return 
    $this->onlineTime;
        }
    And we don't need addOnlineTime(), so let's delete it.

    What if someone calls getOnlineTime() multiple times? It will count the time again! So let's update lastLoadTime to current time every time we read from it: (hence why it is called "lastLoadTime" not "joinTime"):
    PHP:
        public function getOnlineTime() : float{
            
    $micro microtime(true);
            
    $this->onlineTime += $micro $this->lastLoadTime;
            
    $this->lastLoadTime $micro;
            return 
    $this->onlineTime;
        }
    Now we have to update the save function too, to use getters instead of reading the values directly:

    PHP:
        public function save(){
            
    yaml_emit_file($this->getPath(), [
                
    "joins" => $this->getJoins(),
                
    "onlineTime" => $this->getOnlineTime(),
                
    "lastOnline" => $this->getLastOnline()
            ]);
        }
    Done!

    You can find the whole source code of this example plugin on GitHub at https://github.com/SOF3/simple-plugins/tree/master/SessionsExample
     
    Last edited: Dec 15, 2016
    corytortoise likes this.
  3. SkyZone

    SkyZone Slime

    Messages:
    91
    Nice tutorial :D But is there any way to do this easier?
     
    Last edited: Dec 10, 2016
  4. Junkdude

    Junkdude Zombie

    Messages:
    331
    GitHub:
    JunkDaCoder
    Could you add how to make the config an object (from my issue of Get() on string)
     
  5. LilCrispy2o9

    LilCrispy2o9 Spider Jockey

    Messages:
    43
    GitHub:
    lilcrispy2o9
    This is nice, I'm glad you made this so people can get a better practice.
     
  6. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    It is very easy. There are ways to do this more conveniently, but when you want to add more features to your plugin, it gets less convenient. One of the core spirits of computer programming is to "do it once and automate it forever", so never be lazy when you start writing code.

    Don't trust Randall for everything! This is not always true!
    [​IMG]
    What? You just put all properties you use into the class declaration. You don't need to use a Config at all. And the thread already explains it all!
     
    artulloss and Muqsit like this.
  7. xZeroMCPE

    xZeroMCPE Witch

    Messages:
    64
    GitHub:
    xZeroMCPE
    Well, Nice nice tutorial.

    But again, Wouldn't mysql as a data provider be better? Then local saving it.
     
  8. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    No. You don't need to use MySQL for no reason. If you only need to use the data on this server, you have no reason to spare extra trouble storing data into a MySQL storage.
     
  9. Awzaw

    Awzaw Zombie Pigman Poggit Admin Verified

    Messages:
    712
    GitHub:
    awzaw
    This is true is many cases, but not always: for example, if you want to add a command to this plugin that lists the players who spent the most time online, using either SQLite or MySQL would make life a whole lot easier!
     
    Last edited: Dec 13, 2016
    xZeroMCPE and Hipster like this.
  10. Hipster

    Hipster Zombie

    Messages:
    206
    That's true, SQLite 3 is awesome and easy!
     
  11. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    Yeah, but definitely not to require MySQL all the time.
     
    Awzaw likes this.
  12. xZeroMCPE

    xZeroMCPE Witch

    Messages:
    64
    GitHub:
    xZeroMCPE
    In conclusion, there are things that using a "specific" "data provider" would be best for the sake of it.

    But again, depends.
     
  13. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    There can always be more than one specific data provider. SQLite and YAML are not the only two storage formats in this world.
     
    HimbeersaftLP and xZeroMCPE like this.
  14. xZeroMCPE

    xZeroMCPE Witch

    Messages:
    64
    GitHub:
    xZeroMCPE
    Correct.
     
  15. Hipster

    Hipster Zombie

    Messages:
    206
    So true.
     
  16. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    Actually I forgot what is to be continued :p I thought I missed something out, but I am not sure what's wrong now. Any guesses what I didn't add?
     
  17. Hipster

    Hipster Zombie

    Messages:
    206
    PlayerPreLoginEvent
     
  18. SkyZone

    SkyZone Slime

    Messages:
    91
    How can i write a PlayerData and read PlayerData from the main class?
     
  19. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    Oops the plugin is incomplete :)
    Thanks! :)
     
  20. SOFe

    SOFe Administrator Staff Member PMMP Team Poggit Admin Noobiest member in the PMMP Team

    Messages:
    1,796
    GitHub:
    sof3
    Updated the tutorial! Please read the second post for updates.
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.