Class/PanObject.ps1

class PanObject : System.ICloneable {
    [PanDevice] $Device
    [String] $XPath
    [System.Xml.XmlDocument] $XDoc
 
    # Default constructor. If used, must assign properties manually
    PanObject() {
    }

    # Constructor accepting XML content
    PanObject([PanDevice] $Device, [String] $XPath, [System.Xml.XmlDocument] $XDoc) {
        $this.Device = $Device
        $this.XPath = $XPath
        $this.XDoc = $XDoc
    }

    # Static constructor for creating ScriptProperty properties using Update-TypeData
    # Update-TypeData in static contructor is PREFERRED to Add-Member in regular contructor
    # Update-TypeData within static constructor runs ONLY ONCE the first time the type is used is the PowerShell session
    # Contrast with Add-Member within regular constructor runs EVERY TIME a new object is created from the class
    # Be careful choosing where to use linebreaks in the middle of the Update-TypeData cmdlet call. Using linebreaks for getter/setter readability
    static PanObject() {
        # Name ScriptProperty linked to $XDoc.entry.name
        'PanObject' | Update-TypeData -MemberName Name -MemberType ScriptProperty -Value {
        # Getter
            return $this.XDoc.Item('entry').GetAttribute('name')
        } -SecondValue {
        # Setter
            param($Set)
            $this.XDoc.Item('entry').SetAttribute('name',$Set)
        } -Force

        # Tag ScriptProperty linked to $XDoc.entry.tag It's also an array, watch out.
        # <tag><member>tag1</member><member>tag2</member><tag>
        'PanObject' | Update-TypeData -MemberName Tag -MemberType ScriptProperty -Value {
        # Getter
            return $this.XDoc.Item('entry').Item('tag').GetElementsByTagName('member').InnerText
        } -SecondValue {
        # Setter
            param($Set)
            # If <tag> is already present
            if($this.XDoc.Item('entry').Item('tag')) {
                # Clear all <member> (and rebuild later)
                $this.XDoc.Item('entry').Item('tag').RemoveAll()
            }
            # Else <tag> is not present
            else {
                # Build and add <tag>
                $XTag = $this.XDoc.CreateElement('tag')
                $this.XDoc.Item('entry').AppendChild($XTag)
            }
            # Build inner <member>'s
            foreach($TagCur in $Set) {
                $XMember = $this.XDoc.CreateElement('member')
                $XMember.InnerText = $TagCur
                $this.XDoc.Item('entry').Item('tag').AppendChild($XMember)
            }
        } -Force

        # Description ScriptProperty linked to $XDoc.Item('entry').Item('description').InnerText
        'PanObject' | Update-TypeData -MemberName Description -MemberType ScriptProperty -Value {
        # Getter
            return $this.XDoc.Item('entry').Item('description').InnerText
        } -SecondValue {
        # Setter
            param($Set)
            # If <description> element exists
            if($this.XDoc.Item('entry').Item('description')) {
                if([String]::IsNullOrEmpty($Set)) {
                # Remove the <description> element entirely
                $XDescription = $this.XDoc.Item('entry').Item('description')
                $this.XDoc.Item('entry').RemoveChild($XDescription)
                }
                else {
                    $this.XDoc.Item('entry').Item('description').InnerText = $Set
                }
            }
            # No existing <description>
            else {
                # Build a new <description> element
                $XDescription = $this.XDoc.CreateElement('description')
                $XDescription.InnerText = $Set
                $this.XDoc.Item('entry').AppendChild($XDescription)
            }
        } -Force

        # DisableOverride ScriptProperty linked to $XDoc.entry.Item('disable-override').InnerText
        'PanObject' | Update-TypeData -MemberName DisableOverride -MemberType ScriptProperty -Value {
        # Getter
            switch ($this.XDoc.Item('entry').Item('disable-override').InnerText) {
                no       { return $false }
                yes      { return $true }
                default  { return $null }
            }
        } -SecondValue {
        # Setter
            param($Set)
            # If <disable-override> element exists
            if($this.XDoc.Item('entry').Item('disable-override')) {
                if([String]::IsNullOrEmpty($Set)) {
                    # Remove the <disable-override> element entirely
                    $XDisable = $this.XDoc.Item('entry').Item('disable-override')
                    $this.XDoc.Item('entry').RemoveChild($XDisable)
                }
                else {
                    switch($Set) {
                        $false   { $this.XDoc.Item('entry').Item('disable-override').InnerText = 'no' }
                        $true    { $this.XDoc.Item('entry').Item('disable-override').InnerText = 'yes' }
                        # In case someone sets to 'no' or 'yes'
                        no       { $this.XDoc.Item('entry').Item('disable-override').InnerText = 'no' }
                        yes      { $this.XDoc.Item('entry').Item('disable-override').InnerText = 'yes' }
                    }
                }
            }
            # No existing <disable-override>
            else {
                # Build a new <description> element
                $XDisable = $this.XDoc.CreateElement('disable-override')
                switch($Set) {
                    $false   { $XDisable.InnerText = 'no' }
                    $true    { $XDisable.InnerText = 'yes' }
                    # In case someone sets to 'no' or 'yes'
                    no       { $XDisable.InnerText = 'no' }
                    yes      { $XDisable.InnerText = 'yes' }
                }
                $this.XDoc.Item('entry').AppendChild($XDisable)
            }
        } -Force
     
        # Location ScriptProperty linked to $XPath matching
        # Panorama 'MyDeviceGroup' part of device-group/entry[@name='MyDeviceGroup']
        # Ngfw 'vsys1' part of vsys/entry[@name='vsys1']
        # PowerShell regex characters that need escaping [().\^$|?*+{
        'PanObject' | Update-TypeData -MemberName Location -MemberType ScriptProperty -Value {
        # Getter
           # Match includes a capture group to isolate the string literal device-group names or vsys1, vsys2, etc. for actual Location
           # shared
           if($this.XPath -match '/config/shared') { $RegexMatch = "/config/(shared)" }
           # Panorama device-group
           elseif($this.Device.Type -eq [PanDeviceType]::Panorama) { $RegexMatch = "device-group/entry\[@name='(\w+)'\]" }
           # Ngfw vsys
           else { $RegexMatch = "vsys/entry\[@name='(\w+)'\]"}
           # Using PowerShell native -match operator
           if($this.XPath -match $RegexMatch) { return $Matches[1] }
           # Using .NET [Regex]::Match() static method
           # return [Regex]::Match($this.XPath,$RegexMatch).Groups[1].Value
        } -SecondValue {
        # Setter
            param($Set)
            # Replace is the same as Match, but excludes the capture group (parantheses) due to issues encountered
            if($this.XPath -match '/config/shared') {
                # Do nothing
            }
            # Panorama device-group
            if($this.Device.Type -eq [PanDeviceType]::Panorama) {
                $RegexReplace = "device-group/entry\[@name='\w+'\]"
                # Using PowerShell native -replace operator
                $this.XPath = $this.XPath -replace $RegexReplace,("device-group/entry[@name='{0}']" -f $Set)
                # Using .NET [Regex]::Replace() static method
                # $this.XPath = [Regex]::Replace($this.XPath,$RegexReplace,"device-group/entry[@name='{0}']" -f $Set)
            }
            # Ngfw vsys
            else {
                $RegexReplace = "vsys/entry\[@name='\w+'\]"
                # Using PowerShell native -replace operator
                $this.XPath = $this.XPath -replace $RegexReplace,("vsys/entry[@name='{0}']" -f $Set)
                # Using .NET [Regex]::Replace() static method
                # $this.XPath = [Regex]::Replace($this.XPath,$RegexReplace,"vsys/entry[@name='{0}']" -f $Set)
            }
        } -Force
        
        # Overrides ScriptProperty linked to $XDoc.entry.overrides
        'PanObject' | Update-TypeData -MemberName Overrides -MemberType ScriptProperty -Value {
        # Getter
            return $this.XDoc.Item('entry').GetAttribute('overrides')
        } -SecondValue {
        # Setter
            param($Set)
            $this.XDoc.Item('entry').SetAttribute('overrides',$Set)
        } -Force

    } # End static constructor

    # Clone() method as part of ICloneable interface
   [Object] Clone() {
    return [PanObject]::new(
       $this.XDoc.Clone(),
       $this.XPath.Clone(),
       $this.Device
    )
 } # End method
}